diff --git a/Examples/AsyncNotification.ps1 b/Examples/AsyncNotification.ps1 new file mode 100644 index 0000000..2d82f64 --- /dev/null +++ b/Examples/AsyncNotification.ps1 @@ -0,0 +1,84 @@ +# Example: Asynchronous Notification +# This script demonstrates how to show an asynchronous notification and check its status later + +# Load the WindowsNotifications assembly +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "Background Task Running" +$options.Message = "A background task is running. You can continue working." +$options.Async = $true # Run asynchronously +$options.PersistState = $true # Save state to database + +# Add buttons +$viewButton = New-Object WindowsNotifications.Models.NotificationButton("View Progress", "view") +$cancelButton = New-Object WindowsNotifications.Models.NotificationButton("Cancel Task", "cancel") +$options.Buttons.Add($viewButton) +$options.Buttons.Add($cancelButton) + +# Show the notification +Write-Host "Showing asynchronous notification..." +$result = $notificationManager.ShowNotification($options) + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# Simulate doing some work +Write-Host "Performing background work..." +for ($i = 1; $i -le 5; $i++) { + Write-Host " Working... ($i/5)" + Start-Sleep -Seconds 2 + + # Check if the notification has been interacted with + $currentResult = $notificationManager.GetNotificationResult($result.NotificationId) + + if ($currentResult -ne $null -and ($currentResult.Activated -or $currentResult.Dismissed)) { + if ($currentResult.ClickedButtonId -eq "cancel") { + Write-Host "User canceled the task!" + break + } + elseif ($currentResult.ClickedButtonId -eq "view") { + Write-Host "User viewed the progress!" + + # Show a new notification with updated progress + $progressOptions = New-Object WindowsNotifications.Models.NotificationOptions + $progressOptions.Title = "Task Progress" + $progressOptions.Message = "Progress: $i/5 steps completed" + $progressOptions.Tag = "progress" # Group with the same tag + $notificationManager.ShowNotification($progressOptions) + } + } +} + +Write-Host "Background work completed" + +# Show a completion notification +$completionOptions = New-Object WindowsNotifications.Models.NotificationOptions +$completionOptions.Title = "Task Completed" +$completionOptions.Message = "The background task has finished." +$completionOptions.Tag = "progress" # Replace previous notification with same tag +$notificationManager.ShowNotification($completionOptions) + +# Get the final notification result +$finalResult = $notificationManager.GetNotificationResult($result.NotificationId) +if ($finalResult -ne $null) { + Write-Host "Final notification state:" + Write-Host " Activated: $($finalResult.Activated)" + Write-Host " Dismissed: $($finalResult.Dismissed)" + if ($finalResult.ClickedButtonId) { + Write-Host " Button clicked: $($finalResult.ClickedButtonText)" + } +} diff --git a/Examples/CountdownAndDeadline.ps1 b/Examples/CountdownAndDeadline.ps1 new file mode 100644 index 0000000..a9a961b --- /dev/null +++ b/Examples/CountdownAndDeadline.ps1 @@ -0,0 +1,125 @@ +# Example: Countdown and Deadline Notification +# This script demonstrates how to create a notification with a countdown timer and deadline action + +# Load the WindowsNotifications assembly +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "System Maintenance Required" +$options.Message = "Your system needs to restart for maintenance. Please save your work." +$options.PersistState = $true +$options.EnableLogging = $true + +# Set up logging +$logFile = Join-Path -Path $PSScriptRoot -ChildPath "notification_log.txt" +$options.LogAction = { + param($logEntry) + Add-Content -Path $logFile -Value $logEntry + Write-Host $logEntry +} + +# Set deadline (5 minutes from now) +$options.DeadlineTime = (Get-Date).AddMinutes(5) +$options.ShowCountdown = $true + +# Create deadline action (restart computer) +$deadlineAction = [WindowsNotifications.Models.DeadlineAction]::ExecuteScript("Write-Host 'System would restart now (simulated)'; Start-Sleep -Seconds 5") +$options.DeadlineAction = $deadlineAction + +# Add buttons +$restartButton = New-Object WindowsNotifications.Models.NotificationButton("Restart Now", "restart") +$deferButton = New-Object WindowsNotifications.Models.NotificationButton("Defer", "defer") +$options.Buttons.Add($restartButton) +$options.Buttons.Add($deferButton) + +# Configure deferral options +$options.DeferralOptions = New-Object WindowsNotifications.Models.DeferralOptions +$options.DeferralOptions.DeferButtonText = "Defer Restart" +$options.DeferralOptions.DeferralPrompt = "Postpone until:" +$options.DeferralOptions.MaxDeferrals = 2 +$options.DeferralOptions.EnforceMaxDeferrals = $true +$options.DeferralOptions.ScheduleReminder = $true + +# Clear existing deferral choices and add custom ones +$options.DeferralOptions.DeferralChoices.Clear() +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("1 minute", [TimeSpan]::FromMinutes(1), "1min"))) +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("2 minutes", [TimeSpan]::FromMinutes(2), "2min"))) +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("3 minutes", [TimeSpan]::FromMinutes(3), "3min"))) + +# Set up event handlers +$options.OnActivated = { + param($result) + $message = "Notification was activated with button: $($result.ClickedButtonText)" + Write-Host $message + Add-Content -Path $logFile -Value $message + + if ($result.ClickedButtonId -eq "restart") { + Write-Host "User chose to restart now (simulated)" + # In a real script, you would restart the computer here + # Restart-Computer -Force + } +} + +$options.OnTimeout = { + param($result) + $message = "Notification timed out" + Write-Host $message + Add-Content -Path $logFile -Value $message +} + +$options.OnError = { + param($result) + $message = "Error occurred: $($result.ErrorMessage)" + Write-Host $message -ForegroundColor Red + Add-Content -Path $logFile -Value $message +} + +# Show the notification +Write-Host "Showing countdown notification with deadline..." +$result = $notificationManager.ShowNotification($options) + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# Wait for the deadline to pass +Write-Host "Waiting for user interaction or deadline to pass..." +$waitTime = [int]([Math]::Ceiling(($options.DeadlineTime - (Get-Date)).TotalSeconds)) + 5 +Start-Sleep -Seconds $waitTime + +# Get the final result +$finalResult = $notificationManager.GetNotificationResult($result.NotificationId) +if ($finalResult -ne $null) { + Write-Host "`nFinal notification state:" + Write-Host " Activated: $($finalResult.Activated)" + Write-Host " Dismissed: $($finalResult.Dismissed)" + Write-Host " Deferred: $($finalResult.Deferred)" + + if ($finalResult.Deferred) { + Write-Host " Deferred until: $($finalResult.DeferredUntil)" + Write-Host " Deferral reason: $($finalResult.DeferralReason)" + } + + if ($finalResult.DeadlineReached) { + Write-Host " Deadline reached at: $($finalResult.DeadlineReachedTime)" + Write-Host " Deadline action: $($finalResult.DeadlineAction)" + } + + if ($finalResult.ClickedButtonId) { + Write-Host " Button clicked: $($finalResult.ClickedButtonText)" + } +} + +Write-Host "`nLog file: $logFile" diff --git a/Examples/CustomBrandedNotification.ps1 b/Examples/CustomBrandedNotification.ps1 new file mode 100644 index 0000000..db4a09a --- /dev/null +++ b/Examples/CustomBrandedNotification.ps1 @@ -0,0 +1,105 @@ +# Example: Custom Branded Notification +# This script demonstrates how to create a notification with custom branding + +# Load the WindowsNotifications assembly +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options with branding +$options = New-Object WindowsNotifications.Models.NotificationOptions + +# Set basic notification properties +$options.Title = "IT Department Notification" +$options.Message = "Your system has been selected for a security update. Please review the details below." + +# Set branding properties +$options.BrandingText = "Contoso IT Department" +$options.BrandingColor = "#0078D7" # Blue +$options.AccentColor = "#E81123" # Red +$options.UseDarkTheme = $true + +# Set custom images +# Note: Replace these paths with actual image paths on your system +$logoPath = Join-Path -Path $PSScriptRoot -ChildPath "Images\company-logo.png" +if (Test-Path $logoPath) { + $options.LogoImagePath = $logoPath +} +else { + # Use a URL as fallback + $options.LogoImagePath = "https://www.contoso.com/images/logo.png" +} + +$heroPath = Join-Path -Path $PSScriptRoot -ChildPath "Images\banner.png" +if (Test-Path $heroPath) { + $options.HeroImagePath = $heroPath +} + +# Add custom buttons with images +$updateButton = New-Object WindowsNotifications.Models.NotificationButton("Install Update", "install") +$updateButton.BackgroundColor = "#107C10" # Green +$updateButton.TextColor = "#FFFFFF" # White + +$deferButton = New-Object WindowsNotifications.Models.NotificationButton("Defer", "defer") +$deferButton.BackgroundColor = "#5A5A5A" # Gray +$deferButton.TextColor = "#FFFFFF" # White + +$moreInfoButton = New-Object WindowsNotifications.Models.NotificationButton("More Info", "info") +$moreInfoButton.IsContextMenu = $true + +# Add buttons to the notification +$options.Buttons.Add($updateButton) +$options.Buttons.Add($deferButton) +$options.Buttons.Add($moreInfoButton) + +# Configure audio +$options.AudioSource = "ms-winsoundevent:Notification.Default" +$options.SilentMode = $false + +# Show the notification +Write-Host "Showing custom branded notification..." +$result = $notificationManager.ShowNotification($options) + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# If the notification was displayed, wait for interaction +if ($result.Displayed) { + Write-Host "Waiting for user interaction..." + $result = $notificationManager.WaitForNotification($result.NotificationId) + + if ($result -ne $null) { + Write-Host "Notification was activated: $($result.Activated)" + Write-Host "Notification was dismissed: $($result.Dismissed)" + + if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText) (ID: $($result.ClickedButtonId))" + + # Take action based on which button was clicked + switch ($result.ClickedButtonId) { + "install" { + Write-Host "User chose to install the update" + # In a real script, you would initiate the update here + } + "defer" { + Write-Host "User chose to defer the update" + # In a real script, you would schedule a reminder here + } + "info" { + Write-Host "User requested more information" + # In a real script, you would open a help page or display more details + } + } + } + } +} diff --git a/Examples/LoadFromBase64.ps1 b/Examples/LoadFromBase64.ps1 new file mode 100644 index 0000000..be3ad60 --- /dev/null +++ b/Examples/LoadFromBase64.ps1 @@ -0,0 +1,35 @@ +# Example: Load Assembly from Base64 +# This script demonstrates how to load the WindowsNotifications assembly from a Base64 string +# This is useful for embedding the assembly directly in a script + +# In a real scenario, you would replace this with the actual Base64 string of the assembly +# For demonstration purposes, we'll load the DLL and convert it to Base64 +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $base64 = [Convert]::ToBase64String($bytes) + + Write-Host "Assembly loaded and converted to Base64" + Write-Host "Base64 string length: $($base64.Length) characters" + + # In a real script, the base64 string would be hardcoded here + # $base64 = "YOUR_BASE64_STRING_HERE" + + # Load the assembly from the Base64 string + $assemblyBytes = [Convert]::FromBase64String($base64) + $assembly = [System.Reflection.Assembly]::Load($assemblyBytes) + + Write-Host "Assembly loaded successfully: $($assembly.FullName)" + + # Create a notification manager and show a simple notification + $notificationManager = New-Object WindowsNotifications.NotificationManager + $result = $notificationManager.ShowSimpleNotification( + "Loaded from Base64", + "This notification was shown from an assembly loaded from a Base64 string" + ) + + Write-Host "Notification displayed: $($result.Displayed)" +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." +} diff --git a/Examples/NotificationWithButtons.ps1 b/Examples/NotificationWithButtons.ps1 new file mode 100644 index 0000000..ad0ad1f --- /dev/null +++ b/Examples/NotificationWithButtons.ps1 @@ -0,0 +1,58 @@ +# Example: Notification with Buttons +# This script demonstrates how to show a notification with buttons + +# Load the WindowsNotifications assembly +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Show a notification with buttons +Write-Host "Showing notification with buttons..." +$result = $notificationManager.ShowNotificationWithButtons( + "Action Required", + "Please select an option below:", + "Approve", + "Reject", + "Defer" +) + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# If the notification was displayed, wait for interaction +if ($result.Displayed) { + Write-Host "Waiting for user interaction..." + $result = $notificationManager.WaitForNotification($result.NotificationId) + + if ($result -ne $null) { + Write-Host "Notification was activated: $($result.Activated)" + Write-Host "Notification was dismissed: $($result.Dismissed)" + + if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText) (ID: $($result.ClickedButtonId))" + + # Take action based on which button was clicked + switch ($result.ClickedButtonText) { + "Approve" { + Write-Host "User approved the action" + } + "Reject" { + Write-Host "User rejected the action" + } + "Defer" { + Write-Host "User deferred the action" + } + } + } + } +} diff --git a/Examples/PowerShellModule.ps1 b/Examples/PowerShellModule.ps1 new file mode 100644 index 0000000..9a3959c --- /dev/null +++ b/Examples/PowerShellModule.ps1 @@ -0,0 +1,116 @@ +# Example: Using the WindowsNotifications PowerShell Module +# This script demonstrates how to use the WindowsNotifications PowerShell module + +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell" +Import-Module $modulePath -Force + +# Initialize the module +# Note: The DLL must be in the same directory as the module files +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + Write-Host "Initializing WindowsNotifications module with DLL at: $dllPath" + Initialize-WindowsNotifications -AssemblyPath $dllPath +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Check if running as SYSTEM +$isSystem = Test-SystemContext +Write-Host "Running as SYSTEM: $isSystem" + +# Get interactive user sessions +$sessions = Get-InteractiveUserSessions +Write-Host "Interactive user sessions found: $($sessions.Count)" +foreach ($session in $sessions) { + Write-Host " $session" +} + +# Show a simple notification +Write-Host "`nShowing simple notification..." +$result = Show-Notification -Title "Simple Notification" -Message "This is a simple notification from the PowerShell module" + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# Show a notification with buttons +Write-Host "`nShowing notification with buttons..." +$result = Show-Notification -Title "Action Required" -Message "Please select an option:" -Buttons "Approve", "Reject", "Defer" + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText)" + + switch ($result.ClickedButtonText) { + "Approve" { Write-Host "User approved the action" } + "Reject" { Write-Host "User rejected the action" } + "Defer" { Write-Host "User deferred the action" } + } +} + +# Show a reboot notification +Write-Host "`nShowing reboot notification..." +$result = Show-Notification -Title "System Reboot Required" -Message "Your system needs to be rebooted to complete updates." -RebootButtonText "Reboot Now" -DeferButtonText "Defer" + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +if ($result.ClickedButtonId -eq "reboot") { + Write-Host "User chose to reboot now" + # In a real script, you would initiate a reboot here + # Restart-Computer -Force +} +elseif ($result.Deferred) { + Write-Host "User deferred the reboot until: $($result.DeferredUntil)" + Write-Host "Deferral reason: $($result.DeferralReason)" +} + +# Show an asynchronous notification +Write-Host "`nShowing asynchronous notification..." +$result = Show-Notification -Title "Background Task" -Message "A background task is running" -Async -PersistState + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# Simulate doing some work +Write-Host "Performing background work..." +for ($i = 1; $i -le 5; $i++) { + Write-Host " Working... ($i/5)" + Start-Sleep -Seconds 2 + + # Check if the notification has been interacted with + $currentResult = Get-NotificationResult -NotificationId $result.NotificationId + + if ($currentResult -ne $null -and ($currentResult.Activated -or $currentResult.Dismissed)) { + if ($currentResult.ClickedButtonId) { + Write-Host "User clicked: $($currentResult.ClickedButtonText)" + } + else { + Write-Host "User interacted with the notification" + } + } +} + +Write-Host "Background work completed" + +# Get notification history +Write-Host "`nGetting notification history..." +$history = Get-NotificationHistory +Write-Host "Found $($history.Count) notifications in history" + +foreach ($item in $history) { + Write-Host " ID: $($item.NotificationId), Created: $($item.CreatedTime), Activated: $($item.Activated), Dismissed: $($item.Dismissed)" +} + +# Clean up notification history +Write-Host "`nCleaning up notification history..." +$result = Remove-NotificationHistory +Write-Host "Notification history cleared: $result" diff --git a/Examples/RebootNotification.ps1 b/Examples/RebootNotification.ps1 new file mode 100644 index 0000000..af81cc9 --- /dev/null +++ b/Examples/RebootNotification.ps1 @@ -0,0 +1,68 @@ +# Example: Reboot Notification with Deferrals +# This script demonstrates how to show a reboot notification with deferral options + +# Load the WindowsNotifications assembly +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options for a reboot +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "System Reboot Required" +$options.Message = "Your system needs to be rebooted to complete important updates. Please save your work." +$options.PersistState = $true + +# Add reboot button +$rebootButton = New-Object WindowsNotifications.Models.NotificationButton("Reboot Now", "reboot") +$options.Buttons.Add($rebootButton) + +# Configure deferral options +$options.DeferralOptions = New-Object WindowsNotifications.Models.DeferralOptions +$options.DeferralOptions.DeferButtonText = "Defer Reboot" +$options.DeferralOptions.DeferralPrompt = "Postpone until:" +$options.DeferralOptions.MaxDeferrals = 3 + +# Clear existing deferral choices and add custom ones +$options.DeferralOptions.DeferralChoices.Clear() +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("1 hour", [TimeSpan]::FromHours(1), "1hour"))) +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("4 hours", [TimeSpan]::FromHours(4), "4hours"))) +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("End of day", [TimeSpan]::FromHours(8), "endofday"))) +$options.DeferralOptions.DeferralChoices.Add((New-Object WindowsNotifications.Models.DeferralOption("Tomorrow", [TimeSpan]::FromDays(1), "tomorrow"))) + +# Show the notification +Write-Host "Showing reboot notification..." +$result = $notificationManager.ShowNotification($options) + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# If the notification was displayed, wait for interaction +if ($result.Displayed) { + Write-Host "Waiting for user interaction..." + $result = $notificationManager.WaitForNotification($result.NotificationId) + + if ($result -ne $null) { + Write-Host "Notification was activated: $($result.Activated)" + Write-Host "Notification was dismissed: $($result.Dismissed)" + + if ($result.ClickedButtonId -eq "reboot") { + Write-Host "User chose to reboot now" + # In a real script, you would initiate a reboot here + # Restart-Computer -Force + } + elseif ($result.Deferred) { + Write-Host "User deferred the reboot until: $($result.DeferredUntil)" + Write-Host "Deferral reason: $($result.DeferralReason)" + } + } +} diff --git a/Examples/SimpleNotification.ps1 b/Examples/SimpleNotification.ps1 new file mode 100644 index 0000000..17c0a22 --- /dev/null +++ b/Examples/SimpleNotification.ps1 @@ -0,0 +1,49 @@ +# Example: Simple Notification +# This script demonstrates how to show a simple notification + +# Load the WindowsNotifications assembly +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +if (Test-Path $dllPath) { + $bytes = [System.IO.File]::ReadAllBytes($dllPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) +} +else { + Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." + exit +} + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Check if running as SYSTEM +$isSystem = $notificationManager.IsRunningAsSystem() +Write-Host "Running as SYSTEM: $isSystem" + +# Get interactive user sessions +$sessions = $notificationManager.GetInteractiveUserSessions() +Write-Host "Interactive user sessions found: $($sessions.Count)" +foreach ($session in $sessions) { + Write-Host " $session" +} + +# Show a simple notification +Write-Host "Showing simple notification..." +$result = $notificationManager.ShowSimpleNotification("Simple Notification", "This is a simple notification from PowerShell") + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# If the notification was displayed, wait for interaction +if ($result.Displayed) { + Write-Host "Waiting for user interaction..." + $result = $notificationManager.WaitForNotification($result.NotificationId, 30000) + + if ($result -ne $null) { + Write-Host "Notification was activated: $($result.Activated)" + Write-Host "Notification was dismissed: $($result.Dismissed)" + } + else { + Write-Host "Timed out waiting for user interaction" + } +} diff --git a/PowerShell/README.md b/PowerShell/README.md new file mode 100644 index 0000000..6fa1667 --- /dev/null +++ b/PowerShell/README.md @@ -0,0 +1,224 @@ +# WindowsNotifications PowerShell Module + +This PowerShell module provides a convenient way to use the Windows Notifications library from PowerShell scripts. + +## Installation + +1. Copy the module files to one of the following locations: + - `%UserProfile%\Documents\WindowsPowerShell\Modules\WindowsNotifications` (for the current user) + - `%ProgramFiles%\WindowsPowerShell\Modules\WindowsNotifications` (for all users) + +2. Make sure the `WindowsNotifications.dll` file is in the same directory as the module files. + +3. Import the module: + ```powershell + Import-Module WindowsNotifications + ``` + +## Usage + +### Initialize the Module + +Before using the module, you need to initialize it: + +```powershell +# Initialize with default settings +Initialize-WindowsNotifications + +# Or initialize with a custom assembly path +Initialize-WindowsNotifications -AssemblyPath "C:\Path\To\WindowsNotifications.dll" + +# Or initialize with a custom database path +Initialize-WindowsNotifications -DatabasePath "C:\Path\To\Notifications.db" +``` + +### Show Notifications + +#### Simple Notification + +```powershell +Show-Notification -Title "Hello" -Message "This is a simple notification" +``` + +#### Notification with Buttons + +```powershell +$result = Show-Notification -Title "Action Required" -Message "Please select an option:" -Buttons "Approve", "Reject", "Defer" + +if ($result.ClickedButtonText -eq "Approve") { + Write-Host "User approved the action" +} +``` + +#### Reboot Notification with Deferrals + +```powershell +$result = Show-Notification -Title "System Reboot Required" -Message "Your system needs to be rebooted." -RebootButtonText "Reboot Now" -DeferButtonText "Defer" + +if ($result.ClickedButtonId -eq "reboot") { + # Reboot the system + Restart-Computer -Force +} +``` + +#### Advanced Options + +```powershell +Show-Notification -Title "Advanced Notification" -Message "This notification has advanced options" ` + -Async -PersistState -TimeoutInSeconds 30 -LogoImagePath "C:\Path\To\Logo.png" ` + -HeroImagePath "C:\Path\To\Hero.png" -ShowReminder -ReminderTimeInMinutes 5 +``` + +### Work with Notification Results + +```powershell +# Get the result of a notification +$result = Get-NotificationResult -NotificationId "notification-id" + +# Wait for a notification to complete +$result = Wait-Notification -NotificationId "notification-id" -TimeoutInSeconds 30 + +# Get all notification history +$history = Get-NotificationHistory + +# Remove notification history +Remove-NotificationHistory -NotificationId "notification-id" + +# Remove all notification history +Remove-NotificationHistory +``` + +### System Information + +```powershell +# Check if running as SYSTEM +$isSystem = Test-SystemContext + +# Get interactive user sessions +$sessions = Get-InteractiveUserSessions +``` + +## Command Reference + +| Command | Description | +| ------- | ----------- | +| `Initialize-WindowsNotifications` | Initializes the Windows Notifications module | +| `Show-Notification` | Shows a notification with various options | +| `Get-NotificationResult` | Gets the result of a notification | +| `Wait-Notification` | Waits for a notification to complete | +| `Get-NotificationHistory` | Gets all notification results from the database | +| `Remove-NotificationHistory` | Deletes notification results from the database | +| `Get-InteractiveUserSessions` | Gets all interactive user sessions | +| `Test-SystemContext` | Checks if the current process is running as SYSTEM | + +## Custom Branding + +The module supports custom branding for notifications, allowing you to create notifications that match your organization's branding: + +```powershell +Show-Notification -Title "IT Department Notification" -Message "System update required" ` + -BrandingText "Contoso IT" ` + -BrandingColor "#0078D7" ` + -AccentColor "#E81123" ` + -LogoImagePath "C:\Path\To\Logo.png" ` + -HeroImagePath "C:\Path\To\Banner.png" ` + -UseDarkTheme +``` + +### Supported Branding Options + +| Option | Description | +| ------ | ----------- | +| `BrandingText` | Custom branding text (e.g., company or department name) | +| `BrandingColor` | Custom branding color (hex format: #RRGGBB) | +| `AccentColor` | Custom accent color for the notification (hex format: #RRGGBB) | +| `UseDarkTheme` | Whether to use dark theme for the notification | +| `LogoImagePath` | Path or URL to a logo image | +| `HeroImagePath` | Path or URL to a hero/banner image | +| `InlineImagePath` | Path or URL to an inline image | +| `AppIconPath` | Path or URL to an app icon | +| `BackgroundImagePath` | Path or URL to a background image | +| `AudioSource` | Audio source for the notification | +| `SilentMode` | Whether to show the notification in silent mode (no sound) | +| `DeadlineTime` | Deadline time for the notification | +| `ShowCountdown` | Whether to show a countdown timer on the notification | +| `DeadlineActionCommand` | Command to execute when the deadline is reached | +| `DeadlineActionArguments` | Arguments for the deadline command | +| `DeadlineActionUrl` | URL to open when the deadline is reached | +| `DeadlineActionScript` | PowerShell script to execute when the deadline is reached | +| `EnableLogging` | Whether to enable logging for the notification | +| `LogFilePath` | Path to the log file + +## Examples + +### Example 1: Simple Notification + +```powershell +Import-Module WindowsNotifications +Initialize-WindowsNotifications + +$result = Show-Notification -Title "Hello" -Message "This is a simple notification" +Write-Host "Notification displayed: $($result.Displayed)" +``` + +### Example 2: Notification with Buttons + +```powershell +Import-Module WindowsNotifications +Initialize-WindowsNotifications + +$result = Show-Notification -Title "Action Required" -Message "Please select an option:" -Buttons "Approve", "Reject", "Defer" + +if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText)" + + switch ($result.ClickedButtonText) { + "Approve" { Write-Host "User approved the action" } + "Reject" { Write-Host "User rejected the action" } + "Defer" { Write-Host "User deferred the action" } + } +} +``` + +### Example 3: Asynchronous Notification + +```powershell +Import-Module WindowsNotifications +Initialize-WindowsNotifications + +$result = Show-Notification -Title "Background Task" -Message "A background task is running" -Async + +# Do some work +for ($i = 1; $i -le 5; $i++) { + Write-Host "Working... ($i/5)" + Start-Sleep -Seconds 2 + + # Check if the notification has been interacted with + $currentResult = Get-NotificationResult -NotificationId $result.NotificationId + + if ($currentResult.Activated -or $currentResult.Dismissed) { + Write-Host "User interacted with the notification" + break + } +} + +Write-Host "Work completed" +``` + +### Example 4: Countdown and Deadline Notification + +```powershell +Import-Module WindowsNotifications +Initialize-WindowsNotifications + +# Create a notification with a deadline 5 minutes from now +$result = Show-Notification -Title "System Maintenance" -Message "Your system needs to restart soon." ` + -Buttons "Restart Now", "Defer" ` + -DeadlineTime (Get-Date).AddMinutes(5) ` + -ShowCountdown ` + -DeadlineActionScript "Restart-Computer -Force" ` + -EnableLogging ` + -LogFilePath "C:\Logs\notifications.log" + +Write-Host "Notification displayed with deadline: $($result.Displayed)" +``` diff --git a/PowerShell/WindowsNotifications.psd1 b/PowerShell/WindowsNotifications.psd1 new file mode 100644 index 0000000..0ed1904 --- /dev/null +++ b/PowerShell/WindowsNotifications.psd1 @@ -0,0 +1,125 @@ +# +# Module manifest for module 'WindowsNotifications' +# + +@{ + # Script module or binary module file associated with this manifest. + RootModule = 'WindowsNotifications.psm1' + + # Version number of this module. + ModuleVersion = '1.0.0' + + # Supported PSEditions + CompatiblePSEditions = @('Desktop') + + # ID used to uniquely identify this module + GUID = '8a1b3c4d-5e6f-47a8-b9c0-d1e2f3a4b5c6' + + # Author of this module + Author = 'WindowsNotifications' + + # Company or vendor of this module + CompanyName = '' + + # Copyright statement for this module + Copyright = '(c) 2024. All rights reserved.' + + # Description of the functionality provided by this module + Description = 'PowerShell module for displaying Windows Toast Notifications from SYSTEM context to user sessions' + + # Minimum version of the Windows PowerShell engine required by this module + PowerShellVersion = '5.0' + + # Name of the Windows PowerShell host required by this module + # PowerShellHostName = '' + + # Minimum version of the Windows PowerShell host required by this module + # PowerShellHostVersion = '' + + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + DotNetFrameworkVersion = '4.7.2' + + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + CLRVersion = '4.0' + + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' + + # Modules that must be imported into the global environment prior to importing this module + # RequiredModules = @() + + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() + + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() + + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() + + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() + + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() + + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = @( + 'Initialize-WindowsNotifications', + 'Show-Notification', + 'Get-NotificationResult', + 'Wait-Notification', + 'Get-NotificationHistory', + 'Remove-NotificationHistory', + 'Get-InteractiveUserSessions', + 'Test-SystemContext' + ) + + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = @() + + # Variables to export from this module + VariablesToExport = '*' + + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = @() + + # DSC resources to export from this module + # DscResourcesToExport = @() + + # List of all modules packaged with this module + # ModuleList = @() + + # List of all files packaged with this module + FileList = @( + 'WindowsNotifications.psm1', + 'WindowsNotifications.psd1', + 'WindowsNotifications.dll' + ) + + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ + PSData = @{ + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Windows', 'Notifications', 'Toast', 'System') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/freedbygrace/WindowsNotifications/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/freedbygrace/WindowsNotifications' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'Initial release of the WindowsNotifications module' + } + } + + # HelpInfo URI of this module + # HelpInfoURI = '' + + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' +} diff --git a/PowerShell/WindowsNotifications.psm1 b/PowerShell/WindowsNotifications.psm1 new file mode 100644 index 0000000..c89c74a --- /dev/null +++ b/PowerShell/WindowsNotifications.psm1 @@ -0,0 +1,519 @@ +# +# WindowsNotifications.psm1 +# PowerShell module for the Windows Notifications library +# + +# Module variables +$script:NotificationAssembly = $null +$script:NotificationManager = $null +$script:DefaultDatabasePath = $null + +# +# Private functions +# + +function Initialize-NotificationAssembly { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$false)] + [string]$AssemblyPath + ) + + try { + # If assembly path is provided and exists, load from file + if ($AssemblyPath -and (Test-Path $AssemblyPath)) { + $bytes = [System.IO.File]::ReadAllBytes($AssemblyPath) + $script:NotificationAssembly = [System.Reflection.Assembly]::Load($bytes) + } + # Otherwise, try to load from the module directory + else { + $moduleRoot = Split-Path -Parent $PSCommandPath + $defaultPath = Join-Path -Path $moduleRoot -ChildPath "WindowsNotifications.dll" + + if (Test-Path $defaultPath) { + $bytes = [System.IO.File]::ReadAllBytes($defaultPath) + $script:NotificationAssembly = [System.Reflection.Assembly]::Load($bytes) + } + else { + throw "WindowsNotifications.dll not found. Please specify the path using the -AssemblyPath parameter." + } + } + + # Create the notification manager + $script:NotificationManager = New-Object WindowsNotifications.NotificationManager + $script:DefaultDatabasePath = $script:NotificationManager.GetDatabaseFilePath() + + return $true + } + catch { + Write-Error "Failed to initialize WindowsNotifications assembly: $_" + return $false + } +} + +# +# Public functions +# + +function Initialize-WindowsNotifications { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$false)] + [string]$AssemblyPath, + + [Parameter(Mandatory=$false)] + [string]$DatabasePath + ) + + # Initialize the assembly + if (-not (Initialize-NotificationAssembly -AssemblyPath $AssemblyPath)) { + return $false + } + + # If a custom database path is specified, create a new notification manager with that path + if ($DatabasePath) { + try { + $script:NotificationManager = New-Object WindowsNotifications.NotificationManager($DatabasePath) + $script:DefaultDatabasePath = $script:NotificationManager.GetDatabaseFilePath() + } + catch { + Write-Error "Failed to initialize NotificationManager with custom database path: $_" + return $false + } + } + + Write-Host "WindowsNotifications initialized successfully." + Write-Host "Database path: $script:DefaultDatabasePath" + + # Check if running as SYSTEM + $isSystem = $script:NotificationManager.IsRunningAsSystem() + Write-Host "Running as SYSTEM: $isSystem" + + # Get interactive user sessions + $sessions = $script:NotificationManager.GetInteractiveUserSessions() + Write-Host "Interactive user sessions found: $($sessions.Count)" + + return $true +} + +function Show-Notification { + [CmdletBinding(DefaultParameterSetName="Simple")] + param ( + [Parameter(Mandatory=$true, ParameterSetName="Simple")] + [Parameter(Mandatory=$true, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$true, ParameterSetName="Reboot")] + [string]$Title, + + [Parameter(Mandatory=$true, ParameterSetName="Simple")] + [Parameter(Mandatory=$true, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$true, ParameterSetName="Reboot")] + [string]$Message, + + [Parameter(Mandatory=$true, ParameterSetName="WithButtons")] + [string[]]$Buttons, + + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$RebootButtonText = "Reboot Now", + + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$DeferButtonText = "Defer", + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$Async, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$PersistState, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [int]$TimeoutInSeconds = 0, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$LogoImagePath, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$HeroImagePath, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$InlineImagePath, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$AppIconPath, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$BackgroundImagePath, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$BrandingText, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$BrandingColor, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$AccentColor, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$UseDarkTheme, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$AudioSource, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$SilentMode, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$ShowReminder, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [int]$ReminderTimeInMinutes = 60, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [DateTime]$DeadlineTime, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$ShowCountdown, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$DeadlineActionCommand, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$DeadlineActionArguments, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$DeadlineActionUrl, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$DeadlineActionScript, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [switch]$EnableLogging, + + [Parameter(Mandatory=$false, ParameterSetName="Simple")] + [Parameter(Mandatory=$false, ParameterSetName="WithButtons")] + [Parameter(Mandatory=$false, ParameterSetName="Reboot")] + [string]$LogFilePath + ) + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $null + } + } + + try { + # Handle different parameter sets + switch ($PSCmdlet.ParameterSetName) { + "Simple" { + # Create custom notification options for simple notification + $options = New-Object WindowsNotifications.Models.NotificationOptions + $options.Title = $Title + $options.Message = $Message + $options.Async = $Async + $options.PersistState = $PersistState + $options.TimeoutInSeconds = $TimeoutInSeconds + $options.ShowReminder = $ShowReminder + $options.ReminderTimeInMinutes = $ReminderTimeInMinutes + + # Set image paths + if ($LogoImagePath) { $options.LogoImagePath = $LogoImagePath } + if ($HeroImagePath) { $options.HeroImagePath = $HeroImagePath } + if ($InlineImagePath) { $options.InlineImagePath = $InlineImagePath } + if ($AppIconPath) { $options.AppIconPath = $AppIconPath } + if ($BackgroundImagePath) { $options.BackgroundImagePath = $BackgroundImagePath } + + # Set branding options + if ($BrandingText) { $options.BrandingText = $BrandingText } + if ($BrandingColor) { $options.BrandingColor = $BrandingColor } + if ($AccentColor) { $options.AccentColor = $AccentColor } + if ($UseDarkTheme) { $options.UseDarkTheme = $UseDarkTheme } + + # Set audio options + if ($AudioSource) { $options.AudioSource = $AudioSource } + if ($SilentMode) { $options.SilentMode = $SilentMode } + + # Set deadline and countdown options + if ($DeadlineTime -ne $null) { $options.DeadlineTime = $DeadlineTime } + if ($ShowCountdown) { $options.ShowCountdown = $ShowCountdown } + + # Set deadline action + if ($DeadlineActionCommand) { + $options.DeadlineAction = New-Object WindowsNotifications.Models.DeadlineAction($DeadlineActionCommand, $DeadlineActionArguments) + } + elseif ($DeadlineActionUrl) { + $options.DeadlineAction = [WindowsNotifications.Models.DeadlineAction]::OpenUrl($DeadlineActionUrl) + } + elseif ($DeadlineActionScript) { + $options.DeadlineAction = [WindowsNotifications.Models.DeadlineAction]::ExecuteScript($DeadlineActionScript) + } + + # Set logging options + if ($EnableLogging) { $options.EnableLogging = $true } + if ($LogFilePath) { + $options.LogAction = { + param($logEntry) + Add-Content -Path $LogFilePath -Value $logEntry + } + } + + $result = $script:NotificationManager.ShowNotification($options) + } + "WithButtons" { + # Create custom notification options for notification with buttons + $options = New-Object WindowsNotifications.Models.NotificationOptions + $options.Title = $Title + $options.Message = $Message + $options.Async = $Async + $options.PersistState = $PersistState + $options.TimeoutInSeconds = $TimeoutInSeconds + $options.ShowReminder = $ShowReminder + $options.ReminderTimeInMinutes = $ReminderTimeInMinutes + + # Set image paths + if ($LogoImagePath) { $options.LogoImagePath = $LogoImagePath } + if ($HeroImagePath) { $options.HeroImagePath = $HeroImagePath } + if ($InlineImagePath) { $options.InlineImagePath = $InlineImagePath } + if ($AppIconPath) { $options.AppIconPath = $AppIconPath } + if ($BackgroundImagePath) { $options.BackgroundImagePath = $BackgroundImagePath } + + # Set branding options + if ($BrandingText) { $options.BrandingText = $BrandingText } + if ($BrandingColor) { $options.BrandingColor = $BrandingColor } + if ($AccentColor) { $options.AccentColor = $AccentColor } + if ($UseDarkTheme) { $options.UseDarkTheme = $UseDarkTheme } + + # Set audio options + if ($AudioSource) { $options.AudioSource = $AudioSource } + if ($SilentMode) { $options.SilentMode = $SilentMode } + + # Set deadline and countdown options + if ($DeadlineTime -ne $null) { $options.DeadlineTime = $DeadlineTime } + if ($ShowCountdown) { $options.ShowCountdown = $ShowCountdown } + + # Set deadline action + if ($DeadlineActionCommand) { + $options.DeadlineAction = New-Object WindowsNotifications.Models.DeadlineAction($DeadlineActionCommand, $DeadlineActionArguments) + } + elseif ($DeadlineActionUrl) { + $options.DeadlineAction = [WindowsNotifications.Models.DeadlineAction]::OpenUrl($DeadlineActionUrl) + } + elseif ($DeadlineActionScript) { + $options.DeadlineAction = [WindowsNotifications.Models.DeadlineAction]::ExecuteScript($DeadlineActionScript) + } + + # Set logging options + if ($EnableLogging) { $options.EnableLogging = $true } + if ($LogFilePath) { + $options.LogAction = { + param($logEntry) + Add-Content -Path $LogFilePath -Value $logEntry + } + } + + # Add buttons + foreach ($buttonText in $Buttons) { + $button = New-Object WindowsNotifications.Models.NotificationButton($buttonText) + $options.Buttons.Add($button) + } + + $result = $script:NotificationManager.ShowNotification($options) + } + "Reboot" { + # Use the built-in reboot notification method + $result = $script:NotificationManager.ShowRebootNotification($Title, $Message, $RebootButtonText, $DeferButtonText) + } + } + + return $result + } + catch { + Write-Error "Failed to show notification: $_" + return $null + } +} + +function Get-NotificationResult { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$NotificationId + ) + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $null + } + } + + try { + return $script:NotificationManager.GetNotificationResult($NotificationId) + } + catch { + Write-Error "Failed to get notification result: $_" + return $null + } +} + +function Wait-Notification { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$true)] + [string]$NotificationId, + + [Parameter(Mandatory=$false)] + [int]$TimeoutInSeconds = -1 + ) + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $null + } + } + + try { + $timeoutMs = $TimeoutInSeconds -eq -1 ? -1 : ($TimeoutInSeconds * 1000) + return $script:NotificationManager.WaitForNotification($NotificationId, $timeoutMs) + } + catch { + Write-Error "Failed to wait for notification: $_" + return $null + } +} + +function Get-NotificationHistory { + [CmdletBinding()] + param () + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $null + } + } + + try { + return $script:NotificationManager.GetAllNotificationResults() + } + catch { + Write-Error "Failed to get notification history: $_" + return $null + } +} + +function Remove-NotificationHistory { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$false)] + [string]$NotificationId + ) + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $false + } + } + + try { + if ($NotificationId) { + return $script:NotificationManager.DeleteNotificationResult($NotificationId) + } + else { + return $script:NotificationManager.DeleteAllNotificationResults() + } + } + catch { + Write-Error "Failed to remove notification history: $_" + return $false + } +} + +function Get-InteractiveUserSessions { + [CmdletBinding()] + param () + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $null + } + } + + try { + return $script:NotificationManager.GetInteractiveUserSessions() + } + catch { + Write-Error "Failed to get interactive user sessions: $_" + return $null + } +} + +function Test-SystemContext { + [CmdletBinding()] + param () + + # Ensure the notification assembly is initialized + if (-not $script:NotificationManager) { + if (-not (Initialize-WindowsNotifications)) { + return $false + } + } + + try { + return $script:NotificationManager.IsRunningAsSystem() + } + catch { + Write-Error "Failed to check system context: $_" + return $false + } +} + +# Export public functions +Export-ModuleMember -Function Initialize-WindowsNotifications, Show-Notification, Get-NotificationResult, Wait-Notification, Get-NotificationHistory, Remove-NotificationHistory, Get-InteractiveUserSessions, Test-SystemContext diff --git a/README.md b/README.md index dfaff3e..4d1096b 100644 --- a/README.md +++ b/README.md @@ -1 +1,375 @@ -# WindowsNotifications \ No newline at end of file +# Windows Notifications + +A .NET DLL library for PowerShell 5 that displays notifications in user context from SYSTEM, with customization options, deferral support, LiteDB integration, and both synchronous/asynchronous operation modes. + +## Features + +- **User Context Notifications**: Display notifications in the user context from SYSTEM using user impersonation +- **Interactive Session Detection**: Only show notifications when an interactive user session (console or RDP) is present +- **Customizable Notifications**: Create simple or complex notifications with various customization options +- **Custom Branding**: Support for custom branding with logos, images, colors, and themes +- **Deferral Support**: Allow users to defer notifications (e.g., for system reboots) +- **State Persistence**: Save notification state using embedded LiteDB +- **PowerShell Integration**: Easily load and use the library in PowerShell 5 +- **Synchronous/Asynchronous Modes**: Run notifications in blocking or non-blocking mode +- **Countdown Display**: Show countdown timers for time-sensitive notifications +- **Deadline Actions**: Configure custom actions to execute when notification deadlines are reached +- **Logging**: Comprehensive logging of notification events and user interactions + +## Requirements + +- Windows 10 or later +- .NET Framework 4.7.2 or later +- PowerShell 5.0 or later + +## Installation + +1. Download the latest release from the [Releases](https://github.com/freedbygrace/WindowsNotifications/releases) page +2. Extract the ZIP file to a location of your choice +3. Load the assembly in your PowerShell script using one of the methods described below + +## Usage + +### Loading the Assembly + +There are several ways to load the WindowsNotifications assembly in PowerShell: + +#### Method 1: Load from file + +```powershell +$dllPath = "C:\Path\To\WindowsNotifications.dll" +$bytes = [System.IO.File]::ReadAllBytes($dllPath) +$assembly = [System.Reflection.Assembly]::Load($bytes) +``` + +#### Method 2: Load from Base64 string + +```powershell +$base64 = "YOUR_BASE64_STRING_HERE" # Replace with the actual Base64 string of the DLL +$bytes = [Convert]::FromBase64String($base64) +$assembly = [System.Reflection.Assembly]::Load($bytes) +``` + +### Basic Examples + +#### Simple Notification + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Show a simple notification +$result = $notificationManager.ShowSimpleNotification("Title", "Message") +``` + +#### Notification with Buttons + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Show a notification with buttons +$result = $notificationManager.ShowNotificationWithButtons( + "Action Required", + "Please select an option below:", + "Approve", + "Reject", + "Defer" +) + +# Check which button was clicked +if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText)" +} +``` + +#### Reboot Notification with Deferrals + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Show a reboot notification +$result = $notificationManager.ShowRebootNotification( + "System Reboot Required", + "Your system needs to be rebooted to complete updates." +) + +# Check the result +if ($result.ClickedButtonId -eq "reboot") { + # Reboot the system + Restart-Computer -Force +} +elseif ($result.Deferred) { + Write-Host "Reboot deferred until: $($result.DeferredUntil)" +} +``` + +### Advanced Usage + +#### Custom Notification Options + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "Custom Notification" +$options.Message = "This is a custom notification" +$options.TimeoutInSeconds = 30 +$options.Async = $true +$options.PersistState = $true + +# Add buttons +$button1 = New-Object WindowsNotifications.Models.NotificationButton("OK", "ok") +$button2 = New-Object WindowsNotifications.Models.NotificationButton("Cancel", "cancel") +$options.Buttons.Add($button1) +$options.Buttons.Add($button2) + +# Show the notification +$result = $notificationManager.ShowNotification($options) +``` + +#### Asynchronous Notifications + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create notification options with async mode +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "Background Task" +$options.Message = "A background task is running" +$options.Async = $true + +# Show the notification +$result = $notificationManager.ShowNotification($options) + +# Do some work +# ... + +# Check if the notification has been interacted with +$currentResult = $notificationManager.GetNotificationResult($result.NotificationId) +if ($currentResult.Activated) { + Write-Host "User clicked the notification" +} +``` + +#### Custom Database Location + +```powershell +# Create a notification manager with a custom database path +$dbPath = "C:\CustomPath\Notifications.db" +$notificationManager = New-Object WindowsNotifications.NotificationManager($dbPath) + +# Show a notification +$result = $notificationManager.ShowSimpleNotification("Title", "Message") +``` + +#### Custom Branded Notifications + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options with branding +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "IT Department Notification" +$options.Message = "Your system has been selected for a security update." + +# Set branding properties +$options.BrandingText = "Contoso IT Department" +$options.BrandingColor = "#0078D7" # Blue +$options.AccentColor = "#E81123" # Red +$options.UseDarkTheme = $true + +# Set custom images (file paths or URLs) +$options.LogoImagePath = "C:\Path\To\Logo.png" # Or "https://example.com/logo.png" +$options.HeroImagePath = "C:\Path\To\Banner.png" +$options.AppIconPath = "C:\Path\To\Icon.png" + +# Add custom buttons with styling +$updateButton = New-Object WindowsNotifications.Models.NotificationButton("Install Update", "install") +$updateButton.BackgroundColor = "#107C10" # Green +$updateButton.TextColor = "#FFFFFF" # White +$options.Buttons.Add($updateButton) + +# Show the notification +$result = $notificationManager.ShowNotification($options) +``` + +#### Countdown and Deadline Notifications + +```powershell +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Create custom notification options +$options = New-Object WindowsNotifications.Models.NotificationOptions +$options.Title = "System Maintenance Required" +$options.Message = "Your system needs to restart for maintenance. Please save your work." + +# Set deadline (5 minutes from now) +$options.DeadlineTime = (Get-Date).AddMinutes(5) +$options.ShowCountdown = $true + +# Create deadline action (restart computer) +$deadlineAction = [WindowsNotifications.Models.DeadlineAction]::ExecuteScript( + "Write-Host 'System would restart now'; Start-Sleep -Seconds 5" +) +$options.DeadlineAction = $deadlineAction + +# Enable logging +$options.EnableLogging = $true +$options.LogAction = { + param($logEntry) + Add-Content -Path "C:\Logs\notifications.log" -Value $logEntry + Write-Host $logEntry +} + +# Show the notification +$result = $notificationManager.ShowNotification($options) +``` + +## API Reference + +### NotificationManager Class + +The main entry point for the Windows Notifications library. + +#### Constructors + +- `NotificationManager()` - Creates a new NotificationManager with the default database path +- `NotificationManager(string databasePath)` - Creates a new NotificationManager with the specified database path + +#### Methods + +- `NotificationResult ShowNotification(NotificationOptions options)` - Shows a notification with the specified options +- `NotificationResult ShowSimpleNotification(string title, string message)` - Shows a simple notification with the specified title and message +- `NotificationResult ShowNotificationWithButtons(string title, string message, params string[] buttons)` - Shows a notification with buttons +- `NotificationResult ShowRebootNotification(string title, string message, string rebootButtonText = "Reboot Now", string deferButtonText = "Defer")` - Shows a reboot notification with deferral options +- `NotificationResult GetNotificationResult(string notificationId)` - Gets the result of a notification +- `NotificationResult WaitForNotification(string notificationId, int timeout = -1)` - Waits for a notification to complete +- `List GetAllNotificationResults()` - Gets all notification results from the database +- `bool DeleteNotificationResult(string notificationId)` - Deletes a notification result from the database +- `bool DeleteAllNotificationResults()` - Deletes all notification results from the database +- `string GetDatabaseFilePath()` - Gets the path to the database file +- `bool IsRunningAsSystem()` - Checks if the current process is running as SYSTEM +- `List GetInteractiveUserSessions()` - Gets all interactive user sessions + +### NotificationOptions Class + +Options for configuring a notification. + +#### Properties + +- `string Title` - The title of the notification +- `string Message` - The main message body of the notification +- `string LogoImagePath` - Optional logo image path +- `string HeroImagePath` - Optional hero image path +- `string Attribution` - Optional attribution text +- `int TimeoutInSeconds` - Optional timeout in seconds (0 = no timeout) +- `List Buttons` - Optional list of buttons to display +- `bool Async` - Whether to run the notification asynchronously +- `string Id` - Optional unique identifier for the notification +- `string Tag` - Optional tag for grouping notifications +- `string Group` - Optional group name for grouping notifications +- `DeferralOptions DeferralOptions` - Optional deferral options +- `bool ShowReminder` - Whether to show a reminder if the notification is not interacted with +- `int ReminderTimeInMinutes` - Time in minutes after which to show a reminder +- `bool PersistState` - Whether to persist the notification state in the database +- `bool EnableLogging` - Whether to enable logging for this notification +- `Action LogAction` - Optional action to handle logging +- `Action OnActivated` - Optional action to execute when the notification is activated +- `Action OnTimeout` - Optional action to execute when the notification times out +- `Action OnError` - Optional action to execute when an error occurs +- `DateTime? DeadlineTime` - Optional deadline time for the notification +- `DeadlineAction DeadlineAction` - The action to take when the deadline is reached +- `bool ShowCountdown` - Whether to show a countdown timer on the notification + +### NotificationResult Class + +Represents the result of a notification interaction. + +#### Properties + +- `string NotificationId` - The unique identifier of the notification +- `bool Displayed` - Whether the notification was successfully displayed +- `bool Activated` - Whether the notification was activated (clicked) +- `bool Dismissed` - Whether the notification was dismissed +- `string ClickedButtonId` - The ID of the button that was clicked, if any +- `string ClickedButtonText` - The text of the button that was clicked, if any +- `string ClickedButtonArgument` - The argument of the button that was clicked, if any +- `DateTime CreatedTime` - The time when the notification was created +- `DateTime? InteractionTime` - The time when the notification was interacted with, if any +- `string ErrorMessage` - Any error message that occurred during the notification process +- `string ErrorCode` - The error code, if an error occurred +- `bool Deferred` - Whether the notification was deferred +- `DateTime? DeferredUntil` - The time when the notification was deferred until, if applicable +- `string DeferralReason` - The reason for deferral, if applicable +- `string DismissalReason` - The reason for dismissal, if applicable +- `string SystemAction` - The system action that was taken (e.g., snooze, dismiss) +- `bool DeadlineReached` - Whether the deadline was reached +- `DateTime? DeadlineReachedTime` - The time when the deadline was reached, if applicable +- `string DeadlineAction` - The action that was taken when the deadline was reached, if applicable + +## PowerShell Module + +The library includes a PowerShell module that makes it easier to use the Windows Notifications functionality in your PowerShell scripts. The module provides cmdlets for showing notifications, checking results, and managing notification history. + +### Installing the PowerShell Module + +1. Copy the files from the `PowerShell` directory to one of the following locations: + - `%UserProfile%\Documents\WindowsPowerShell\Modules\WindowsNotifications` (for the current user) + - `%ProgramFiles%\WindowsPowerShell\Modules\WindowsNotifications` (for all users) + +2. Make sure the `WindowsNotifications.dll` file is in the same directory as the module files. + +3. Import the module: + ```powershell + Import-Module WindowsNotifications + ``` + +### Using the PowerShell Module + +```powershell +# Initialize the module +Initialize-WindowsNotifications + +# Show a simple notification +Show-Notification -Title "Hello" -Message "This is a simple notification" + +# Show a notification with buttons +$result = Show-Notification -Title "Action Required" -Message "Please select an option:" -Buttons "Approve", "Reject", "Defer" + +# Show a reboot notification +$result = Show-Notification -Title "System Reboot Required" -Message "Your system needs to be rebooted." -RebootButtonText "Reboot Now" -DeferButtonText "Defer" +``` + +See the [PowerShell/README.md](PowerShell/README.md) file for more information about the PowerShell module. + +## Examples + +See the [Examples](Examples) directory for complete PowerShell script examples: + +- [SimpleNotification.ps1](Examples/SimpleNotification.ps1) - Shows a simple notification +- [NotificationWithButtons.ps1](Examples/NotificationWithButtons.ps1) - Shows a notification with buttons +- [RebootNotification.ps1](Examples/RebootNotification.ps1) - Shows a reboot notification with deferral options +- [AsyncNotification.ps1](Examples/AsyncNotification.ps1) - Shows an asynchronous notification +- [LoadFromBase64.ps1](Examples/LoadFromBase64.ps1) - Demonstrates loading the assembly from a Base64 string +- [PowerShellModule.ps1](Examples/PowerShellModule.ps1) - Demonstrates using the PowerShell module +- [CustomBrandedNotification.ps1](Examples/CustomBrandedNotification.ps1) - Demonstrates custom branding options +- [CountdownAndDeadline.ps1](Examples/CountdownAndDeadline.ps1) - Demonstrates countdown timers and deadline actions + +## Building from Source + +1. Clone the repository +2. Open the solution in Visual Studio +3. Restore NuGet packages +4. Build the solution in Release mode +5. The compiled DLL will be in the `WindowsNotifications\bin\Release` directory + +## License + +This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details. \ No newline at end of file diff --git a/WindowsNotifications.sln b/WindowsNotifications.sln new file mode 100644 index 0000000..d54fb3d --- /dev/null +++ b/WindowsNotifications.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsNotifications", "WindowsNotifications\WindowsNotifications.csproj", "{A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F1E2D3C4-B5A6-47C8-B9D0-E1F2A3B4C5D6} + EndGlobalSection +EndGlobal diff --git a/WindowsNotifications/Models/DeadlineAction.cs b/WindowsNotifications/Models/DeadlineAction.cs new file mode 100644 index 0000000..a63c224 --- /dev/null +++ b/WindowsNotifications/Models/DeadlineAction.cs @@ -0,0 +1,172 @@ +using System; +using System.Diagnostics; + +namespace WindowsNotifications.Models +{ + /// + /// Represents an action to take when a notification deadline is reached + /// + public class DeadlineAction + { + /// + /// The type of action to take + /// + public DeadlineActionType ActionType { get; set; } = DeadlineActionType.None; + + /// + /// The command to execute (for Process action type) + /// + public string Command { get; set; } + + /// + /// The arguments for the command (for Process action type) + /// + public string Arguments { get; set; } + + /// + /// The URL to open (for Url action type) + /// + public string Url { get; set; } + + /// + /// The script to execute (for Script action type) + /// + public string Script { get; set; } + + /// + /// The custom action to execute (for Custom action type) + /// + public Action CustomAction { get; set; } + + /// + /// Creates a new DeadlineAction with no action + /// + public DeadlineAction() + { + } + + /// + /// Creates a new DeadlineAction that runs a process + /// + /// The command to execute + /// The arguments for the command + public DeadlineAction(string command, string arguments = null) + { + ActionType = DeadlineActionType.Process; + Command = command; + Arguments = arguments; + } + + /// + /// Creates a new DeadlineAction that opens a URL + /// + /// The URL to open + public static DeadlineAction OpenUrl(string url) + { + return new DeadlineAction + { + ActionType = DeadlineActionType.Url, + Url = url + }; + } + + /// + /// Creates a new DeadlineAction that executes a PowerShell script + /// + /// The PowerShell script to execute + public static DeadlineAction ExecuteScript(string script) + { + return new DeadlineAction + { + ActionType = DeadlineActionType.Script, + Script = script + }; + } + + /// + /// Creates a new DeadlineAction that executes a custom action + /// + /// The custom action to execute + public static DeadlineAction ExecuteCustomAction(Action action) + { + return new DeadlineAction + { + ActionType = DeadlineActionType.Custom, + CustomAction = action + }; + } + + /// + /// Executes the deadline action + /// + /// The notification result + public void Execute(NotificationResult result) + { + try + { + switch (ActionType) + { + case DeadlineActionType.Process: + if (!string.IsNullOrEmpty(Command)) + { + Process.Start(Command, Arguments ?? string.Empty); + } + break; + + case DeadlineActionType.Url: + if (!string.IsNullOrEmpty(Url)) + { + Process.Start(Url); + } + break; + + case DeadlineActionType.Script: + if (!string.IsNullOrEmpty(Script)) + { + Process.Start("powershell.exe", $"-ExecutionPolicy Bypass -Command \"{Script}\""); + } + break; + + case DeadlineActionType.Custom: + CustomAction?.Invoke(result); + break; + } + } + catch (Exception ex) + { + Debug.WriteLine($"Error executing deadline action: {ex.Message}"); + } + } + } + + /// + /// The type of action to take when a deadline is reached + /// + public enum DeadlineActionType + { + /// + /// No action + /// + None, + + /// + /// Run a process + /// + Process, + + /// + /// Open a URL + /// + Url, + + /// + /// Execute a PowerShell script + /// + Script, + + /// + /// Execute a custom action + /// + Custom + } +} diff --git a/WindowsNotifications/Models/DeferralOptions.cs b/WindowsNotifications/Models/DeferralOptions.cs new file mode 100644 index 0000000..d169bac --- /dev/null +++ b/WindowsNotifications/Models/DeferralOptions.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; + +namespace WindowsNotifications.Models +{ + /// + /// Options for configuring notification deferrals + /// + public class DeferralOptions + { + /// + /// Whether deferrals are enabled for this notification + /// + public bool Enabled { get; set; } = true; + + /// + /// The maximum number of times the notification can be deferred + /// + public int MaxDeferrals { get; set; } = 3; + + /// + /// The available deferral options to show to the user + /// + public List DeferralChoices { get; set; } = new List(); + + /// + /// The text to display for the deferral button + /// + public string DeferButtonText { get; set; } = "Defer"; + + /// + /// The text to display for the deferral selection prompt + /// + public string DeferralPrompt { get; set; } = "Defer until:"; + + /// + /// Whether to schedule a reminder when the notification is deferred + /// + public bool ScheduleReminder { get; set; } = true; + + /// + /// Whether to enforce the maximum number of deferrals + /// + public bool EnforceMaxDeferrals { get; set; } = true; + + /// + /// The action to take when the maximum number of deferrals is reached + /// + public DeadlineAction MaxDeferralsAction { get; set; } + + /// + /// The current number of times this notification has been deferred + /// + public int CurrentDeferralCount { get; set; } = 0; + + /// + /// Creates a new DeferralOptions instance with default values + /// + public DeferralOptions() + { + // Add default deferral choices + DeferralChoices.Add(new DeferralOption("30 minutes", TimeSpan.FromMinutes(30))); + DeferralChoices.Add(new DeferralOption("1 hour", TimeSpan.FromHours(1))); + DeferralChoices.Add(new DeferralOption("4 hours", TimeSpan.FromHours(4))); + DeferralChoices.Add(new DeferralOption("Tomorrow", TimeSpan.FromDays(1))); + + // Set default max deferrals action + MaxDeferralsAction = new DeadlineAction(); + } + + /// + /// Creates a new DeferralOptions instance with custom deferral choices + /// + /// The deferral choices to offer + public DeferralOptions(List deferralChoices) + { + DeferralChoices = deferralChoices; + } + } + + /// + /// Represents a single deferral option + /// + public class DeferralOption + { + /// + /// The text to display for this deferral option + /// + public string Text { get; set; } + + /// + /// The time span to defer for + /// + public TimeSpan DeferralTime { get; set; } + + /// + /// Optional identifier for this deferral option + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Creates a new deferral option + /// + /// The text to display + /// The time span to defer for + /// Optional identifier + public DeferralOption(string text, TimeSpan deferralTime, string id = null) + { + Text = text; + DeferralTime = deferralTime; + if (!string.IsNullOrEmpty(id)) + Id = id; + } + } +} diff --git a/WindowsNotifications/Models/NotificationOptions.cs b/WindowsNotifications/Models/NotificationOptions.cs new file mode 100644 index 0000000..4037aa5 --- /dev/null +++ b/WindowsNotifications/Models/NotificationOptions.cs @@ -0,0 +1,304 @@ +using System; +using System.Collections.Generic; + +namespace WindowsNotifications.Models +{ + /// + /// Options for configuring a notification + /// + public class NotificationOptions + { + /// + /// The title of the notification + /// + public string Title { get; set; } + + /// + /// The main message body of the notification + /// + public string Message { get; set; } + + /// + /// Optional logo image path or URL + /// + public string LogoImagePath { get; set; } + + /// + /// Optional hero image path or URL + /// + public string HeroImagePath { get; set; } + + /// + /// Optional inline image path or URL + /// + public string InlineImagePath { get; set; } + + /// + /// Optional app icon path or URL + /// + public string AppIconPath { get; set; } + + /// + /// Optional badge logo path or URL + /// + public string BadgeLogoPath { get; set; } + + /// + /// Optional background image path or URL for the toast + /// + public string BackgroundImagePath { get; set; } + + /// + /// Optional attribution text + /// + public string Attribution { get; set; } + + /// + /// Optional timeout in seconds. If set to 0, the notification will not timeout. + /// + public int TimeoutInSeconds { get; set; } = 0; + + /// + /// Optional list of buttons to display on the notification + /// + public List Buttons { get; set; } = new List(); + + /// + /// Whether to run the notification asynchronously + /// + public bool Async { get; set; } = false; + + /// + /// Optional scenario for the notification (alarm, reminder, incomingCall, urgent) + /// + public string Scenario { get; set; } + + /// + /// Optional launch argument for the notification + /// + public string LaunchArgument { get; set; } + + /// + /// Optional audio source for the notification + /// + public string AudioSource { get; set; } + + /// + /// Whether to loop the audio + /// + public bool LoopAudio { get; set; } = false; + + /// + /// Optional custom branding text (e.g., company or department name) + /// + public string BrandingText { get; set; } + + /// + /// Optional custom branding color (hex format: #RRGGBB) + /// + public string BrandingColor { get; set; } + + /// + /// Optional custom accent color for the notification (hex format: #RRGGBB) + /// + public string AccentColor { get; set; } + + /// + /// Optional custom font family for the notification text + /// + public string FontFamily { get; set; } + + /// + /// Whether to use dark theme for the notification + /// + public bool UseDarkTheme { get; set; } = false; + + /// + /// Whether to show the notification in silent mode (no sound) + /// + public bool SilentMode { get; set; } = false; + + /// + /// Whether to show a progress bar on the notification + /// + public bool ShowProgressBar { get; set; } = false; + + /// + /// The progress value (0-100) for the progress bar + /// + public int ProgressValue { get; set; } = 0; + + /// + /// The status text for the progress bar + /// + public string ProgressStatus { get; set; } + + /// + /// Whether to show a countdown timer on the notification + /// + public bool ShowCountdown { get; set; } = false; + + /// + /// The deadline time for the notification + /// + public DateTime? DeadlineTime { get; set; } + + /// + /// The action to take when the deadline is reached + /// + public DeadlineAction DeadlineAction { get; set; } + + /// + /// Optional unique identifier for the notification + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Optional tag for grouping notifications + /// + public string Tag { get; set; } + + /// + /// Optional group name for grouping notifications + /// + public string Group { get; set; } + + /// + /// Optional deferral options for notifications that can be deferred + /// + public DeferralOptions DeferralOptions { get; set; } + + /// + /// Whether to show a reminder if the notification is not interacted with + /// + public bool ShowReminder { get; set; } = false; + + /// + /// Time in minutes after which to show a reminder if ShowReminder is true + /// + public int ReminderTimeInMinutes { get; set; } = 60; + + /// + /// Whether to persist the notification state in the database + /// + public bool PersistState { get; set; } = false; + + /// + /// Whether to enable logging for this notification + /// + public bool EnableLogging { get; set; } = true; + + /// + /// Optional action to handle logging (if null, logs to Debug output) + /// + public Action LogAction { get; set; } + + /// + /// Optional action to execute when the notification is activated + /// + public Action OnActivated { get; set; } + + /// + /// Optional action to execute when the notification times out + /// + public Action OnTimeout { get; set; } + + /// + /// Optional action to execute when an error occurs + /// + public Action OnError { get; set; } + + /// + /// Optional deadline time for the notification + /// + public DateTime? DeadlineTime { get; set; } + + /// + /// The action to take when the deadline is reached + /// + public DeadlineAction DeadlineAction { get; set; } + + /// + /// Whether to show a countdown timer on the notification + /// + public bool ShowCountdown { get; set; } = false; + + /// + /// Optional serialized data for reminders + /// + public string ReminderData { get; set; } + } + + /// + /// Represents a button on a notification + /// + public class NotificationButton + { + /// + /// The text to display on the button + /// + public string Text { get; set; } + + /// + /// Optional identifier for the button + /// + public string Id { get; set; } = Guid.NewGuid().ToString(); + + /// + /// Optional argument to pass when the button is clicked + /// + public string Argument { get; set; } + + /// + /// Optional image URI for the button (file path or URL) + /// + public string ImageUri { get; set; } + + /// + /// Whether this button should appear in the context menu + /// + public bool IsContextMenu { get; set; } = false; + + /// + /// Optional tooltip text for the button + /// + public string ToolTip { get; set; } + + /// + /// Optional background color for the button (hex format: #RRGGBB) + /// + public string BackgroundColor { get; set; } + + /// + /// Optional text color for the button (hex format: #RRGGBB) + /// + public string TextColor { get; set; } + + /// + /// Creates a new notification button with the specified text + /// + /// The text to display on the button + /// Optional identifier for the button + /// Optional argument to pass when the button is clicked + public NotificationButton(string text, string id = null, string argument = null) + { + Text = text; + if (!string.IsNullOrEmpty(id)) + Id = id; + Argument = argument ?? Id; + } + + /// + /// Creates a new notification button with the specified text and image + /// + /// The text to display on the button + /// The image URI for the button (file path or URL) + /// Optional identifier for the button + /// Optional argument to pass when the button is clicked + public NotificationButton(string text, string imageUri, string id = null, string argument = null) + : this(text, id, argument) + { + ImageUri = imageUri; + } + } +} diff --git a/WindowsNotifications/Models/NotificationResult.cs b/WindowsNotifications/Models/NotificationResult.cs new file mode 100644 index 0000000..32cca07 --- /dev/null +++ b/WindowsNotifications/Models/NotificationResult.cs @@ -0,0 +1,129 @@ +using System; + +namespace WindowsNotifications.Models +{ + /// + /// Represents the result of a notification interaction + /// + public class NotificationResult + { + /// + /// The unique identifier of the notification + /// + public string NotificationId { get; set; } + + /// + /// Whether the notification was successfully displayed + /// + public bool Displayed { get; set; } + + /// + /// Whether the notification was activated (clicked) + /// + public bool Activated { get; set; } + + /// + /// Whether the notification was dismissed + /// + public bool Dismissed { get; set; } + + /// + /// The ID of the button that was clicked, if any + /// + public string ClickedButtonId { get; set; } + + /// + /// The text of the button that was clicked, if any + /// + public string ClickedButtonText { get; set; } + + /// + /// The argument of the button that was clicked, if any + /// + public string ClickedButtonArgument { get; set; } + + /// + /// The time when the notification was created + /// + public DateTime CreatedTime { get; set; } = DateTime.Now; + + /// + /// The time when the notification was interacted with, if any + /// + public DateTime? InteractionTime { get; set; } + + /// + /// Any error message that occurred during the notification process + /// + public string ErrorMessage { get; set; } + + /// + /// The error code, if an error occurred + /// + public string ErrorCode { get; set; } + + /// + /// Whether the notification was deferred + /// + public bool Deferred { get; set; } + + /// + /// The time when the notification was deferred until, if applicable + /// + public DateTime? DeferredUntil { get; set; } + + /// + /// The reason for deferral, if applicable + /// + public string DeferralReason { get; set; } + + /// + /// The reason for dismissal, if applicable + /// + public string DismissalReason { get; set; } + + /// + /// The system action that was taken (e.g., snooze, dismiss) + /// + public string SystemAction { get; set; } + + /// + /// Whether the deadline was reached + /// + public bool DeadlineReached { get; set; } + + /// + /// The time when the deadline was reached, if applicable + /// + public DateTime? DeadlineReachedTime { get; set; } + + /// + /// The action that was taken when the deadline was reached, if applicable + /// + public string DeadlineAction { get; set; } + + /// + /// Creates a new notification result with the specified notification ID + /// + /// The unique identifier of the notification + public NotificationResult(string notificationId) + { + NotificationId = notificationId; + } + + /// + /// Creates a new error notification result + /// + /// The unique identifier of the notification + /// The error message + /// A notification result with the error message + public static NotificationResult Error(string notificationId, string errorMessage) + { + return new NotificationResult(notificationId) + { + ErrorMessage = errorMessage, + Displayed = false + }; + } + } +} diff --git a/WindowsNotifications/NotificationManager.cs b/WindowsNotifications/NotificationManager.cs new file mode 100644 index 0000000..a947c00 --- /dev/null +++ b/WindowsNotifications/NotificationManager.cs @@ -0,0 +1,387 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using WindowsNotifications.Models; +using WindowsNotifications.Services; + +namespace WindowsNotifications +{ + /// + /// Main entry point for the Windows Notifications library + /// + public class NotificationManager + { + private readonly UserSessionManager _sessionManager; + private readonly ToastNotificationService _toastService; + private readonly DatabaseService _databaseService; + private readonly Dictionary _reminderTimers = new Dictionary(); + private readonly Dictionary _deadlineTimers = new Dictionary(); + private readonly Timer _cleanupTimer; + + /// + /// Gets or sets the path to the LiteDB database file + /// + public string DatabasePath { get; private set; } + + /// + /// Creates a new NotificationManager with the default database path + /// + public NotificationManager() : this(null) + { + } + + /// + /// Creates a new NotificationManager with the specified database path + /// + /// The path to the LiteDB database file, or null to use the default + public NotificationManager(string databasePath) + { + DatabasePath = databasePath; + _sessionManager = new UserSessionManager(); + _toastService = new ToastNotificationService(); + _databaseService = new DatabaseService(databasePath); + + // Initialize cleanup timer to run every hour + _cleanupTimer = new Timer(CleanupExpiredNotifications, null, TimeSpan.FromHours(1), TimeSpan.FromHours(1)); + } + + /// + /// Shows a notification with the specified options + /// + /// The notification options + /// The result of the notification + public NotificationResult ShowNotification(NotificationOptions options) + { + // Validate options + if (options == null) + throw new ArgumentNullException(nameof(options)); + + if (string.IsNullOrEmpty(options.Title) && string.IsNullOrEmpty(options.Message)) + throw new ArgumentException("Either Title or Message must be specified"); + + // Get interactive user sessions + var sessions = _sessionManager.GetInteractiveUserSessions(); + if (sessions.Count == 0) + return NotificationResult.Error(options.Id, "No interactive user sessions found"); + + // Show the notification in the first interactive session + var session = sessions.First(); + NotificationResult result = null; + + using (var impersonation = new UserImpersonation()) + { + try + { + result = impersonation.ExecuteAsUser(session.SessionId, () => _toastService.ShowNotification(options)); + } + catch (Exception ex) + { + result = NotificationResult.Error(options.Id, $"Failed to show notification: {ex.Message}"); + } + } + + // Set up a reminder if requested + if (options.ShowReminder && result.Displayed && !result.Activated && !result.Dismissed) + { + SetupReminderTimer(options); + } + + // Set up a deadline timer if specified + if (options.DeadlineTime.HasValue && result.Displayed && !result.Activated && !result.Dismissed) + { + SetupDeadlineTimer(options, result); + } + + // Persist the result if requested + if (options.PersistState && result != null) + { + _databaseService.SaveNotificationResult(result); + } + + return result; + } + + /// + /// Shows a simple notification with the specified title and message + /// + /// The title of the notification + /// The message body of the notification + /// The result of the notification + public NotificationResult ShowSimpleNotification(string title, string message) + { + var options = new NotificationOptions + { + Title = title, + Message = message + }; + + return ShowNotification(options); + } + + /// + /// Shows a notification with buttons + /// + /// The title of the notification + /// The message body of the notification + /// The buttons to display + /// The result of the notification + public NotificationResult ShowNotificationWithButtons(string title, string message, params string[] buttons) + { + var options = new NotificationOptions + { + Title = title, + Message = message, + Buttons = buttons.Select(b => new NotificationButton(b)).ToList() + }; + + return ShowNotification(options); + } + + /// + /// Shows a reboot notification with deferral options + /// + /// The title of the notification + /// The message body of the notification + /// The text for the reboot button + /// The text for the defer button + /// The result of the notification + public NotificationResult ShowRebootNotification(string title, string message, string rebootButtonText = "Reboot Now", string deferButtonText = "Defer") + { + var options = new NotificationOptions + { + Title = title, + Message = message, + Buttons = new List { new NotificationButton(rebootButtonText, "reboot") }, + DeferralOptions = new DeferralOptions { DeferButtonText = deferButtonText }, + PersistState = true + }; + + return ShowNotification(options); + } + + /// + /// Gets the result of a notification + /// + /// The ID of the notification + /// The notification result, or null if not found + public NotificationResult GetNotificationResult(string notificationId) + { + // Try to get from the toast service first + var result = _toastService.GetNotificationResult(notificationId); + + // If not found, try to get from the database + if (result == null) + result = _databaseService.GetNotificationResult(notificationId); + + return result; + } + + /// + /// Waits for a notification to complete + /// + /// The ID of the notification + /// The timeout in milliseconds, or -1 to wait indefinitely + /// The notification result, or null if timed out or not found + public NotificationResult WaitForNotification(string notificationId, int timeout = -1) + { + return _toastService.WaitForNotification(notificationId, timeout); + } + + /// + /// Gets all notification results from the database + /// + /// A list of notification results + public List GetAllNotificationResults() + { + return _databaseService.GetAllNotificationResults(); + } + + /// + /// Deletes a notification result from the database + /// + /// The ID of the notification + /// True if the deletion was successful, false otherwise + public bool DeleteNotificationResult(string notificationId) + { + return _databaseService.DeleteNotificationResult(notificationId); + } + + /// + /// Deletes all notification results from the database + /// + /// True if the deletion was successful, false otherwise + public bool DeleteAllNotificationResults() + { + return _databaseService.DeleteAllNotificationResults(); + } + + /// + /// Gets the path to the database file + /// + /// The database file path + public string GetDatabaseFilePath() + { + return _databaseService.GetDatabaseFilePath(); + } + + /// + /// Sets up a reminder timer for a notification + /// + /// The notification options + private void SetupReminderTimer(NotificationOptions options) + { + // Create a reminder timer + var timer = new Timer(state => + { + var reminderOptions = new NotificationOptions + { + Title = $"Reminder: {options.Title}", + Message = options.Message, + Buttons = options.Buttons, + Async = options.Async, + Tag = options.Tag, + Group = options.Group, + LogoImagePath = options.LogoImagePath, + HeroImagePath = options.HeroImagePath, + Attribution = options.Attribution, + TimeoutInSeconds = options.TimeoutInSeconds, + PersistState = options.PersistState + }; + + ShowNotification(reminderOptions); + }, null, options.ReminderTimeInMinutes * 60 * 1000, Timeout.Infinite); + + // Store the timer + _reminderTimers[options.Id] = timer; + } + + /// + /// Sets up a deadline timer for a notification + /// + /// The notification options + /// The notification result + private void SetupDeadlineTimer(NotificationOptions options, NotificationResult result) + { + if (!options.DeadlineTime.HasValue) + return; + + // Calculate time until deadline + TimeSpan timeUntilDeadline = options.DeadlineTime.Value - DateTime.Now; + if (timeUntilDeadline.TotalMilliseconds <= 0) + return; // Deadline already passed + + // Create a deadline timer + var timer = new Timer(state => + { + try + { + // Get the latest result from the database if persisted + NotificationResult currentResult = options.PersistState + ? _databaseService.GetNotificationResult(options.Id) ?? result + : result; + + // If the notification has already been interacted with, do nothing + if (currentResult.Activated || currentResult.Dismissed) + return; + + // Update the result + currentResult.DeadlineReached = true; + currentResult.DeadlineReachedTime = DateTime.Now; + currentResult.DeadlineAction = options.DeadlineAction?.ActionType.ToString() ?? "None"; + + // Execute the deadline action if specified + if (options.DeadlineAction != null && options.DeadlineAction.ActionType != DeadlineActionType.None) + { + options.DeadlineAction.Execute(currentResult); + } + + // Persist the updated result if requested + if (options.PersistState) + { + _databaseService.SaveNotificationResult(currentResult); + } + + // Log the deadline reached event + if (options.EnableLogging && options.LogAction != null) + { + string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [DeadlineReached] ID: {options.Id}, Title: {options.Title}, Action: {currentResult.DeadlineAction}"; + options.LogAction(logEntry); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error executing deadline action: {ex.Message}"); + } + }, null, (int)timeUntilDeadline.TotalMilliseconds, Timeout.Infinite); + + // Store the timer + _deadlineTimers[options.Id] = timer; + } + + /// + /// Cleans up expired notifications from the database + /// + private void CleanupExpiredNotifications(object state) + { + try + { + // Get all notification results from the database + var results = _databaseService.GetAllNotificationResults(); + if (results == null || results.Count == 0) + return; + + // Find expired notifications (older than 30 days) + var expiredResults = results.Where(r => r.CreatedTime < DateTime.Now.AddDays(-30)).ToList(); + if (expiredResults.Count == 0) + return; + + // Delete expired notifications + foreach (var result in expiredResults) + { + _databaseService.DeleteNotificationResult(result.NotificationId); + + // Clean up any associated timers + if (_reminderTimers.ContainsKey(result.NotificationId)) + { + _reminderTimers[result.NotificationId].Dispose(); + _reminderTimers.Remove(result.NotificationId); + } + + if (_deadlineTimers.ContainsKey(result.NotificationId)) + { + _deadlineTimers[result.NotificationId].Dispose(); + _deadlineTimers.Remove(result.NotificationId); + } + } + + System.Diagnostics.Debug.WriteLine($"Cleaned up {expiredResults.Count} expired notifications"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error cleaning up expired notifications: {ex.Message}"); + } + } + + /// + /// Checks if the current process is running as SYSTEM + /// + /// True if running as SYSTEM, false otherwise + public bool IsRunningAsSystem() + { + return UserImpersonation.IsRunningAsSystem; + } + + /// + /// Gets all interactive user sessions + /// + /// A list of interactive user sessions + public List GetInteractiveUserSessions() + { + return _sessionManager.GetInteractiveUserSessions() + .Select(s => s.ToString()) + .ToList(); + } + } +} diff --git a/WindowsNotifications/Properties/AssemblyInfo.cs b/WindowsNotifications/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..ce253d2 --- /dev/null +++ b/WindowsNotifications/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("WindowsNotifications")] +[assembly: AssemblyDescription("Windows Notification Library for PowerShell")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WindowsNotifications")] +[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("a1b2c3d4-e5f6-47a8-b9c0-d1e2f3a4b5c6")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/WindowsNotifications/Resources/LiteDB.dll b/WindowsNotifications/Resources/LiteDB.dll new file mode 100644 index 0000000..213eeec Binary files /dev/null and b/WindowsNotifications/Resources/LiteDB.dll differ diff --git a/WindowsNotifications/Services/DatabaseService.cs b/WindowsNotifications/Services/DatabaseService.cs new file mode 100644 index 0000000..7b638ab --- /dev/null +++ b/WindowsNotifications/Services/DatabaseService.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using WindowsNotifications.Models; +using WindowsNotifications.Utils; + +namespace WindowsNotifications.Services +{ + /// + /// Service for managing notification state persistence using LiteDB + /// + internal class DatabaseService + { + private readonly string _dbPath; + private readonly LiteDBEmbedded _liteDb; + private const string DEFAULT_DB_FOLDER = "%PROGRAMDATA%\\WindowsNotifications"; + private const string DEFAULT_DB_NAME = "notifications.db"; + + /// + /// Creates a new DatabaseService with the specified database path + /// + /// The path to the database file, or null to use the default + public DatabaseService(string dbPath = null) + { + // Determine the database path + _dbPath = GetDatabasePath(dbPath); + + // Ensure the directory exists + string directory = Path.GetDirectoryName(_dbPath); + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + // Initialize LiteDB + _liteDb = new LiteDBEmbedded(_dbPath); + } + + /// + /// Gets the database path, using the default if none is specified + /// + /// The specified database path, or null to use the default + /// The resolved database path + private string GetDatabasePath(string dbPath) + { + if (!string.IsNullOrEmpty(dbPath)) + return dbPath; + + string folder = Environment.ExpandEnvironmentVariables(DEFAULT_DB_FOLDER); + return Path.Combine(folder, DEFAULT_DB_NAME); + } + + /// + /// Saves a notification result to the database + /// + /// The notification result to save + /// True if the save was successful, false otherwise + public bool SaveNotificationResult(NotificationResult result) + { + try + { + return _liteDb.UpsertNotificationResult(result); + } + catch (Exception) + { + return false; + } + } + + /// + /// Gets a notification result from the database + /// + /// The ID of the notification + /// The notification result, or null if not found + public NotificationResult GetNotificationResult(string notificationId) + { + try + { + return _liteDb.GetNotificationResult(notificationId); + } + catch (Exception) + { + return null; + } + } + + /// + /// Gets all notification results from the database + /// + /// A list of notification results + public List GetAllNotificationResults() + { + try + { + return _liteDb.GetAllNotificationResults(); + } + catch (Exception) + { + return new List(); + } + } + + /// + /// Deletes a notification result from the database + /// + /// The ID of the notification + /// True if the deletion was successful, false otherwise + public bool DeleteNotificationResult(string notificationId) + { + try + { + return _liteDb.DeleteNotificationResult(notificationId); + } + catch (Exception) + { + return false; + } + } + + /// + /// Deletes all notification results from the database + /// + /// True if the deletion was successful, false otherwise + public bool DeleteAllNotificationResults() + { + try + { + return _liteDb.DeleteAllNotificationResults(); + } + catch (Exception) + { + return false; + } + } + + /// + /// Gets the path to the database file + /// + /// The database file path + public string GetDatabaseFilePath() + { + return _dbPath; + } + } +} diff --git a/WindowsNotifications/Services/ToastNotificationService.cs b/WindowsNotifications/Services/ToastNotificationService.cs new file mode 100644 index 0000000..a5347fa --- /dev/null +++ b/WindowsNotifications/Services/ToastNotificationService.cs @@ -0,0 +1,643 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; +using Windows.Data.Xml.Dom; +using Windows.UI.Notifications; +using WindowsNotifications.Models; + +namespace WindowsNotifications.Services +{ + /// + /// Service for creating and displaying Windows Toast Notifications + /// + internal class ToastNotificationService + { + private const string APP_ID = "WindowsNotifications.PowerShell"; + private readonly Dictionary _notificationEvents = new Dictionary(); + private readonly Dictionary _notificationResults = new Dictionary(); + + /// + /// Shows a toast notification with the specified options + /// + /// The notification options + /// The result of the notification + public NotificationResult ShowNotification(NotificationOptions options) + { + var result = new NotificationResult(options.Id); + + try + { + // Register COM server for notifications + RegisterComServer(); + + // Create the toast XML + string toastXml = CreateToastXml(options); + XmlDocument xmlDoc = new XmlDocument(); + xmlDoc.LoadXml(toastXml); + + // Create the toast notification + ToastNotification toast = new ToastNotification(xmlDoc); + + // Set up event handlers + var resetEvent = new ManualResetEvent(false); + _notificationEvents[options.Id] = resetEvent; + _notificationResults[options.Id] = result; + + // Register event handlers + toast.Activated += (sender, args) => OnToastActivated(sender, args, options); + toast.Dismissed += (sender, args) => OnToastDismissed(sender, args, options); + toast.Failed += (sender, args) => OnToastFailed(sender, args, options); + + // Show the notification + ToastNotificationManager.CreateToastNotifier(APP_ID).Show(toast); + result.Displayed = true; + + // If not async, wait for the notification to be interacted with + if (!options.Async) + { + resetEvent.WaitOne(); + result = _notificationResults[options.Id]; + } + + return result; + } + catch (Exception ex) + { + return NotificationResult.Error(options.Id, ex.Message); + } + } + + /// + /// Gets the result of a notification + /// + /// The ID of the notification + /// The notification result, or null if not found + public NotificationResult GetNotificationResult(string notificationId) + { + if (_notificationResults.ContainsKey(notificationId)) + return _notificationResults[notificationId]; + + return null; + } + + /// + /// Waits for a notification to complete + /// + /// The ID of the notification + /// The timeout in milliseconds, or -1 to wait indefinitely + /// The notification result, or null if timed out or not found + public NotificationResult WaitForNotification(string notificationId, int timeout = -1) + { + if (!_notificationEvents.ContainsKey(notificationId)) + return null; + + var resetEvent = _notificationEvents[notificationId]; + if (resetEvent.WaitOne(timeout)) + return _notificationResults[notificationId]; + + return null; + } + + /// + /// Creates the XML for a toast notification + /// + /// The notification options + /// The toast XML + private string CreateToastXml(NotificationOptions options) + { + // Create the XML document + XmlDocument doc = new XmlDocument(); + + // Create the toast element + XmlElement toastElement = doc.CreateElement("toast"); + doc.AppendChild(toastElement); + + // Add optional attributes + if (!string.IsNullOrEmpty(options.Tag)) + toastElement.SetAttribute("tag", options.Tag); + + if (!string.IsNullOrEmpty(options.Group)) + toastElement.SetAttribute("group", options.Group); + + if (options.TimeoutInSeconds > 0) + toastElement.SetAttribute("duration", options.TimeoutInSeconds <= 30 ? "short" : "long"); + + // Add scenario if specified + if (!string.IsNullOrEmpty(options.Scenario)) + toastElement.SetAttribute("scenario", options.Scenario); + + // Add launch attribute if specified + if (!string.IsNullOrEmpty(options.LaunchArgument)) + toastElement.SetAttribute("launch", options.LaunchArgument); + + // Create the visual element + XmlElement visualElement = doc.CreateElement("visual"); + toastElement.AppendChild(visualElement); + + // Add branding color if specified + if (!string.IsNullOrEmpty(options.BrandingColor)) + visualElement.SetAttribute("baseUri", options.BrandingColor); + + // Add accent color if specified + if (!string.IsNullOrEmpty(options.AccentColor)) + visualElement.SetAttribute("addImageQuery", options.AccentColor); + + // Create the binding element + XmlElement bindingElement = doc.CreateElement("binding"); + bindingElement.SetAttribute("template", "ToastGeneric"); + visualElement.AppendChild(bindingElement); + + // Add dark theme if specified + if (options.UseDarkTheme) + bindingElement.SetAttribute("hint-darkTheme", "true"); + + // Add custom font family if specified + if (!string.IsNullOrEmpty(options.FontFamily)) + bindingElement.SetAttribute("hint-textStacking", options.FontFamily); + + // Add title and message + if (!string.IsNullOrEmpty(options.Title)) + { + XmlElement titleElement = doc.CreateElement("text"); + titleElement.InnerText = options.Title; + bindingElement.AppendChild(titleElement); + } + + if (!string.IsNullOrEmpty(options.Message)) + { + XmlElement messageElement = doc.CreateElement("text"); + messageElement.InnerText = options.Message; + bindingElement.AppendChild(messageElement); + } + + // Add countdown timer if enabled + if (options.ShowCountdown && options.DeadlineTime.HasValue) + { + TimeSpan timeLeft = options.DeadlineTime.Value - DateTime.Now; + if (timeLeft.TotalSeconds > 0) + { + string countdownText = $"Time remaining: {(int)timeLeft.TotalHours}h {timeLeft.Minutes}m {timeLeft.Seconds}s"; + XmlElement countdownElement = doc.CreateElement("text"); + countdownElement.InnerText = countdownText; + bindingElement.AppendChild(countdownElement); + } + } + + // Add images if specified + // Logo image (small app logo) + if (!string.IsNullOrEmpty(options.LogoImagePath)) + { + bool isUrl = options.LogoImagePath.StartsWith("http", StringComparison.OrdinalIgnoreCase); + if (isUrl || File.Exists(options.LogoImagePath)) + { + XmlElement logoElement = doc.CreateElement("image"); + logoElement.SetAttribute("placement", "appLogoOverride"); + logoElement.SetAttribute("src", options.LogoImagePath); + logoElement.SetAttribute("hint-crop", "circle"); + bindingElement.AppendChild(logoElement); + } + } + + // Hero image (large banner image) + if (!string.IsNullOrEmpty(options.HeroImagePath)) + { + bool isUrl = options.HeroImagePath.StartsWith("http", StringComparison.OrdinalIgnoreCase); + if (isUrl || File.Exists(options.HeroImagePath)) + { + XmlElement heroElement = doc.CreateElement("image"); + heroElement.SetAttribute("placement", "hero"); + heroElement.SetAttribute("src", options.HeroImagePath); + bindingElement.AppendChild(heroElement); + } + } + + // Inline image (image within the notification content) + if (!string.IsNullOrEmpty(options.InlineImagePath)) + { + bool isUrl = options.InlineImagePath.StartsWith("http", StringComparison.OrdinalIgnoreCase); + if (isUrl || File.Exists(options.InlineImagePath)) + { + XmlElement inlineElement = doc.CreateElement("image"); + inlineElement.SetAttribute("src", options.InlineImagePath); + bindingElement.AppendChild(inlineElement); + } + } + + // App icon (icon displayed in the notification) + if (!string.IsNullOrEmpty(options.AppIconPath)) + { + bool isUrl = options.AppIconPath.StartsWith("http", StringComparison.OrdinalIgnoreCase); + if (isUrl || File.Exists(options.AppIconPath)) + { + XmlElement appIconElement = doc.CreateElement("image"); + appIconElement.SetAttribute("placement", "appIcon"); + appIconElement.SetAttribute("src", options.AppIconPath); + bindingElement.AppendChild(appIconElement); + } + } + + // Background image (background of the entire toast) + if (!string.IsNullOrEmpty(options.BackgroundImagePath)) + { + bool isUrl = options.BackgroundImagePath.StartsWith("http", StringComparison.OrdinalIgnoreCase); + if (isUrl || File.Exists(options.BackgroundImagePath)) + { + XmlElement backgroundElement = doc.CreateElement("image"); + backgroundElement.SetAttribute("placement", "background"); + backgroundElement.SetAttribute("src", options.BackgroundImagePath); + bindingElement.AppendChild(backgroundElement); + } + } + + // Add attribution if specified + if (!string.IsNullOrEmpty(options.Attribution)) + { + XmlElement attributionElement = doc.CreateElement("text"); + attributionElement.SetAttribute("placement", "attribution"); + attributionElement.InnerText = options.Attribution; + bindingElement.AppendChild(attributionElement); + } + + // Add branding text if specified + if (!string.IsNullOrEmpty(options.BrandingText)) + { + XmlElement brandingElement = doc.CreateElement("text"); + brandingElement.SetAttribute("placement", "attribution"); + brandingElement.InnerText = options.BrandingText; + bindingElement.AppendChild(brandingElement); + } + + // Add progress bar if specified + if (options.ShowProgressBar && options.ProgressValue >= 0 && options.ProgressValue <= 100) + { + XmlElement progressElement = doc.CreateElement("progress"); + progressElement.SetAttribute("value", (options.ProgressValue / 100.0).ToString("0.00")); + progressElement.SetAttribute("status", options.ProgressStatus ?? "Progress"); + bindingElement.AppendChild(progressElement); + } + + // Add actions if there are buttons or deferrals + if (options.Buttons.Count > 0 || (options.DeferralOptions != null && options.DeferralOptions.Enabled)) + { + XmlElement actionsElement = doc.CreateElement("actions"); + toastElement.AppendChild(actionsElement); + + // Add deferral button if enabled + if (options.DeferralOptions != null && options.DeferralOptions.Enabled) + { + // Create the input element for deferral selection + XmlElement inputElement = doc.CreateElement("input"); + inputElement.SetAttribute("id", "deferralSelect"); + inputElement.SetAttribute("type", "selection"); + inputElement.SetAttribute("title", options.DeferralOptions.DeferralPrompt); + actionsElement.AppendChild(inputElement); + + // Add deferral options + foreach (var deferOption in options.DeferralOptions.DeferralChoices) + { + XmlElement selectionElement = doc.CreateElement("selection"); + selectionElement.SetAttribute("id", deferOption.Id); + selectionElement.SetAttribute("content", deferOption.Text); + inputElement.AppendChild(selectionElement); + } + + // Add the defer action button + XmlElement deferActionElement = doc.CreateElement("action"); + deferActionElement.SetAttribute("activationType", "system"); + deferActionElement.SetAttribute("arguments", "defer"); + deferActionElement.SetAttribute("content", options.DeferralOptions.DeferButtonText); + deferActionElement.SetAttribute("hint-inputId", "deferralSelect"); + actionsElement.AppendChild(deferActionElement); + } + + // Add regular buttons + foreach (var button in options.Buttons) + { + XmlElement actionElement = doc.CreateElement("action"); + actionElement.SetAttribute("activationType", "foreground"); + actionElement.SetAttribute("arguments", button.Id); + actionElement.SetAttribute("content", button.Text); + + // Add optional button attributes + if (!string.IsNullOrEmpty(button.ImageUri)) + actionElement.SetAttribute("imageUri", button.ImageUri); + + if (button.IsContextMenu) + actionElement.SetAttribute("placement", "contextMenu"); + + actionsElement.AppendChild(actionElement); + } + } + + // Add audio element if specified + if (!string.IsNullOrEmpty(options.AudioSource) || options.SilentMode) + { + XmlElement audioElement = doc.CreateElement("audio"); + + if (options.SilentMode) + { + audioElement.SetAttribute("silent", "true"); + } + else if (!string.IsNullOrEmpty(options.AudioSource)) + { + audioElement.SetAttribute("src", options.AudioSource); + + if (options.LoopAudio) + audioElement.SetAttribute("loop", "true"); + } + + toastElement.AppendChild(audioElement); + } + + // Return the XML as a string + return doc.GetXml(); + } + + /// + /// Escapes XML special characters + /// + /// The text to escape + /// The escaped text + private string EscapeXml(string text) + { + return text + .Replace("&", "&") + .Replace("<", "<") + .Replace(">", ">") + .Replace("\"", """) + .Replace("'", "'"); + } + + /// + /// Handles toast activation events + /// + private void OnToastActivated(ToastNotification sender, object args, NotificationOptions options) + { + if (!_notificationResults.ContainsKey(options.Id)) + return; + + var result = _notificationResults[options.Id]; + result.Activated = true; + result.InteractionTime = DateTime.Now; + + // Handle button clicks + if (args is ToastActivatedEventArgs activatedArgs) + { + string argument = activatedArgs.Arguments; + + // Check if this is a custom action + if (argument == "snooze" || argument == "dismiss") + { + // Handle system actions + result.SystemAction = argument; + } + else + { + // Handle custom button clicks + var button = options.Buttons.FirstOrDefault(b => b.Id == argument); + + if (button != null) + { + result.ClickedButtonId = button.Id; + result.ClickedButtonText = button.Text; + result.ClickedButtonArgument = button.Argument; + } + + // Execute custom action if specified + if (options.OnActivated != null) + { + try + { + options.OnActivated(result); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error executing OnActivated action: {ex.Message}"); + } + } + } + } + + // Log the activation + LogNotificationEvent(options, "Activated", result.ClickedButtonText ?? "Direct activation"); + + // Signal that the notification has been interacted with + if (_notificationEvents.ContainsKey(options.Id)) + _notificationEvents[options.Id].Set(); + } + + /// + /// Handles toast dismissal events + /// + private void OnToastDismissed(ToastNotification sender, ToastDismissedEventArgs args, NotificationOptions options) + { + if (!_notificationResults.ContainsKey(options.Id)) + return; + + var result = _notificationResults[options.Id]; + result.Dismissed = true; + result.InteractionTime = DateTime.Now; + result.DismissalReason = args.Reason.ToString(); + + string dismissReason = "Unknown"; + + // Handle different dismissal reasons + switch (args.Reason) + { + case ToastDismissalReason.ApplicationHidden: + dismissReason = "Application hidden"; + break; + + case ToastDismissalReason.UserCanceled: + dismissReason = "User dismissed"; + + // Handle deferrals + if (options.DeferralOptions != null && options.DeferralOptions.Enabled) + { + // Get the selected deferral option + if (args is ToastDismissedEventArgs deferArgs && deferArgs.GetDeferralSelectionId() != null) + { + string deferralId = deferArgs.GetDeferralSelectionId(); + var deferOption = options.DeferralOptions.DeferralChoices.FirstOrDefault(d => d.Id == deferralId); + + if (deferOption != null) + { + result.Deferred = true; + result.DeferredUntil = DateTime.Now.Add(deferOption.DeferralTime); + result.DeferralReason = deferOption.Text; + dismissReason = $"Deferred until {result.DeferredUntil} ({deferOption.Text})"; + + // Schedule a reminder if needed + if (options.DeferralOptions.ScheduleReminder) + { + ScheduleDeferralReminder(options, deferOption); + } + } + } + } + break; + + case ToastDismissalReason.TimedOut: + dismissReason = "Timed out"; + + // Execute timeout action if specified + if (options.OnTimeout != null) + { + try + { + options.OnTimeout(result); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error executing OnTimeout action: {ex.Message}"); + } + } + break; + } + + // Log the dismissal + LogNotificationEvent(options, "Dismissed", dismissReason); + + // Signal that the notification has been interacted with + if (_notificationEvents.ContainsKey(options.Id)) + _notificationEvents[options.Id].Set(); + } + + /// + /// Handles toast failure events + /// + private void OnToastFailed(ToastNotification sender, ToastFailedEventArgs args, NotificationOptions options) + { + if (!_notificationResults.ContainsKey(options.Id)) + return; + + var result = _notificationResults[options.Id]; + result.ErrorMessage = args.ErrorMessage; + result.ErrorCode = args.ErrorCode.ToString(); + + // Log the failure + LogNotificationEvent(options, "Failed", $"Error: {args.ErrorMessage} (Code: {args.ErrorCode})"); + + // Execute error action if specified + if (options.OnError != null) + { + try + { + options.OnError(result); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error executing OnError action: {ex.Message}"); + } + } + + // Signal that the notification has failed + if (_notificationEvents.ContainsKey(options.Id)) + _notificationEvents[options.Id].Set(); + } + + /// + /// Logs a notification event + /// + /// The notification options + /// The type of event + /// Additional details about the event + private void LogNotificationEvent(NotificationOptions options, string eventType, string details) + { + if (!options.EnableLogging) + return; + + try + { + // Get the current user session + var sessionManager = new UserSessionManager(); + var currentUser = sessionManager.GetCurrentInteractiveUser(); + string userInfo = currentUser != null ? $"{currentUser.DomainName}\\{currentUser.UserName} (Session: {currentUser.SessionId})" : "Unknown user"; + + // Create the log entry + string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] [{eventType}] ID: {options.Id}, Title: {options.Title}, User: {userInfo}, Details: {details}"; + + // Log to the specified action + if (options.LogAction != null) + { + options.LogAction(logEntry); + } + else + { + // Default logging to debug output + System.Diagnostics.Debug.WriteLine(logEntry); + } + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error logging notification event: {ex.Message}"); + } + } + + /// + /// Schedules a reminder for a deferred notification + /// + /// The notification options + /// The selected deferral option + private void ScheduleDeferralReminder(NotificationOptions options, DeferralOption deferOption) + { + try + { + // Calculate the reminder time + DateTime reminderTime = DateTime.Now.Add(deferOption.DeferralTime); + + // Create a task scheduler task + using (TaskService ts = new TaskService()) + { + // Create a new task definition + TaskDefinition td = ts.NewTask(); + td.RegistrationInfo.Description = $"Reminder for notification: {options.Title}"; + + // Create a trigger that will fire at the reminder time + td.Triggers.Add(new TimeTrigger(reminderTime)); + + // Create an action to show the reminder notification + string actionPath = System.Diagnostics.Process.GetCurrentProcess().MainModule.FileName; + string actionArgs = $"-ExecutionPolicy Bypass -Command \"& {{ $bytes = [System.Convert]::FromBase64String('{options.ReminderData}'); $assembly = [System.Reflection.Assembly]::Load($bytes); $notificationManager = New-Object WindowsNotifications.NotificationManager; $options = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::Deserialize([System.IO.MemoryStream]::new($bytes)); $notificationManager.ShowNotification($options); }}\""; + + td.Actions.Add(new ExecAction(actionPath, actionArgs, null)); + + // Register the task + ts.RootFolder.RegisterTaskDefinition($"WindowsNotification_Reminder_{options.Id}", td); + } + + // Log the scheduled reminder + LogNotificationEvent(options, "ReminderScheduled", $"Reminder scheduled for {reminderTime}"); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine($"Error scheduling deferral reminder: {ex.Message}"); + } + } + + /// + /// Registers the COM server for notifications + /// + private void RegisterComServer() + { + try + { + // Register the AppID for the current process + string appUserModelId = APP_ID; + SetCurrentProcessExplicitAppUserModelID(appUserModelId); + } + catch (Exception ex) + { + throw new Exception("Failed to register COM server for notifications", ex); + } + } + + [DllImport("shell32.dll", SetLastError = true)] + private static extern int SetCurrentProcessExplicitAppUserModelID([MarshalAs(UnmanagedType.LPWStr)] string AppID); + } +} diff --git a/WindowsNotifications/Services/UserImpersonation.cs b/WindowsNotifications/Services/UserImpersonation.cs new file mode 100644 index 0000000..7da3e05 --- /dev/null +++ b/WindowsNotifications/Services/UserImpersonation.cs @@ -0,0 +1,280 @@ +using System; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.ComponentModel; +using System.Security.Permissions; + +namespace WindowsNotifications.Services +{ + /// + /// Provides functionality for impersonating a user from SYSTEM context + /// + internal class UserImpersonation : IDisposable + { + #region Win32 API + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool LogonUser( + string lpszUsername, + string lpszDomain, + string lpszPassword, + int dwLogonType, + int dwLogonProvider, + out IntPtr phToken); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool DuplicateToken( + IntPtr ExistingTokenHandle, + int ImpersonationLevel, + out IntPtr DuplicateTokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool ImpersonateLoggedOnUser(IntPtr hToken); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool RevertToSelf(); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern bool CloseHandle(IntPtr hObject); + + [DllImport("wtsapi32.dll", SetLastError = true)] + private static extern bool WTSQueryUserToken(uint sessionId, out IntPtr phToken); + + [DllImport("userenv.dll", SetLastError = true)] + private static extern bool CreateEnvironmentBlock(out IntPtr lpEnvironment, IntPtr hToken, bool bInherit); + + [DllImport("userenv.dll", SetLastError = true)] + private static extern bool DestroyEnvironmentBlock(IntPtr lpEnvironment); + + private const int LOGON32_LOGON_INTERACTIVE = 2; + private const int LOGON32_PROVIDER_DEFAULT = 0; + private const int LOGON32_PROVIDER_WINNT50 = 3; + private const int SECURITY_IMPERSONATION = 2; + + #endregion + + private IntPtr _userToken = IntPtr.Zero; + private IntPtr _impersonationToken = IntPtr.Zero; + private IntPtr _environmentBlock = IntPtr.Zero; + private bool _disposed = false; + private bool _isImpersonating = false; + + /// + /// Gets whether the current process is running as SYSTEM + /// + public static bool IsRunningAsSystem + { + get + { + using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) + { + return identity.User.IsWellKnown(WellKnownSidType.LocalSystemSid); + } + } + } + + /// + /// Impersonates a user by session ID + /// + /// The session ID to impersonate + /// True if impersonation was successful, false otherwise + public bool ImpersonateBySessionId(int sessionId) + { + if (_isImpersonating) + return false; + + try + { + if (!WTSQueryUserToken((uint)sessionId, out _userToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to get user token for session"); + } + + if (!DuplicateToken(_userToken, SECURITY_IMPERSONATION, out _impersonationToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to duplicate token"); + } + + if (!CreateEnvironmentBlock(out _environmentBlock, _impersonationToken, false)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create environment block"); + } + + if (!ImpersonateLoggedOnUser(_impersonationToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to impersonate user"); + } + + _isImpersonating = true; + return true; + } + catch (Exception ex) + { + CleanupTokens(); + throw new Exception("Impersonation failed", ex); + } + } + + /// + /// Impersonates a user by username and password + /// + /// The username to impersonate + /// The domain of the user + /// The password of the user + /// True if impersonation was successful, false otherwise + public bool ImpersonateUser(string username, string domain, string password) + { + if (_isImpersonating) + return false; + + try + { + if (!LogonUser(username, domain, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out _userToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to log on as user"); + } + + if (!DuplicateToken(_userToken, SECURITY_IMPERSONATION, out _impersonationToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to duplicate token"); + } + + if (!CreateEnvironmentBlock(out _environmentBlock, _impersonationToken, false)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create environment block"); + } + + if (!ImpersonateLoggedOnUser(_impersonationToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to impersonate user"); + } + + _isImpersonating = true; + return true; + } + catch (Exception ex) + { + CleanupTokens(); + throw new Exception("Impersonation failed", ex); + } + } + + /// + /// Stops impersonating the user and reverts to the original security context + /// + public void StopImpersonation() + { + if (_isImpersonating) + { + RevertToSelf(); + _isImpersonating = false; + } + + CleanupTokens(); + } + + /// + /// Executes an action while impersonating a user + /// + /// The session ID to impersonate + /// The action to execute + /// True if the action was executed successfully, false otherwise + public bool ExecuteAsUser(int sessionId, Action action) + { + if (!ImpersonateBySessionId(sessionId)) + return false; + + try + { + action(); + return true; + } + finally + { + StopImpersonation(); + } + } + + /// + /// Executes a function while impersonating a user and returns the result + /// + /// The return type of the function + /// The session ID to impersonate + /// The function to execute + /// The result of the function + public T ExecuteAsUser(int sessionId, Func func) + { + if (!ImpersonateBySessionId(sessionId)) + throw new Exception("Failed to impersonate user"); + + try + { + return func(); + } + finally + { + StopImpersonation(); + } + } + + /// + /// Cleans up any open tokens + /// + private void CleanupTokens() + { + if (_environmentBlock != IntPtr.Zero) + { + DestroyEnvironmentBlock(_environmentBlock); + _environmentBlock = IntPtr.Zero; + } + + if (_impersonationToken != IntPtr.Zero) + { + CloseHandle(_impersonationToken); + _impersonationToken = IntPtr.Zero; + } + + if (_userToken != IntPtr.Zero) + { + CloseHandle(_userToken); + _userToken = IntPtr.Zero; + } + } + + /// + /// Disposes of resources + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Disposes of resources + /// + /// Whether this is being called from Dispose() or the finalizer + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + // Dispose managed resources + } + + // Dispose unmanaged resources + StopImpersonation(); + _disposed = true; + } + } + + /// + /// Finalizer + /// + ~UserImpersonation() + { + Dispose(false); + } + } +} diff --git a/WindowsNotifications/Services/UserSessionManager.cs b/WindowsNotifications/Services/UserSessionManager.cs new file mode 100644 index 0000000..97aeab8 --- /dev/null +++ b/WindowsNotifications/Services/UserSessionManager.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Linq; +using System.Security.Principal; +using System.Diagnostics; + +namespace WindowsNotifications.Services +{ + /// + /// Manages user sessions and detects interactive sessions + /// + internal class UserSessionManager + { + #region Win32 API + + [DllImport("wtsapi32.dll")] + private static extern bool WTSEnumerateSessions( + IntPtr hServer, + int Reserved, + int Version, + ref IntPtr ppSessionInfo, + ref int pCount); + + [DllImport("wtsapi32.dll")] + private static extern void WTSFreeMemory(IntPtr pMemory); + + [DllImport("wtsapi32.dll")] + private static extern bool WTSQuerySessionInformation( + IntPtr hServer, + int sessionId, + WTS_INFO_CLASS wtsInfoClass, + out IntPtr ppBuffer, + out int pBytesReturned); + + [DllImport("kernel32.dll")] + private static extern uint WTSGetActiveConsoleSessionId(); + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_SESSION_INFO + { + public int SessionId; + [MarshalAs(UnmanagedType.LPStr)] + public string pWinStationName; + public WTS_CONNECTSTATE_CLASS State; + } + + private enum WTS_INFO_CLASS + { + WTSInitialProgram = 0, + WTSApplicationName = 1, + WTSWorkingDirectory = 2, + WTSOEMId = 3, + WTSSessionId = 4, + WTSUserName = 5, + WTSWinStationName = 6, + WTSDomainName = 7, + WTSConnectState = 8, + WTSClientBuildNumber = 9, + WTSClientName = 10, + WTSClientDirectory = 11, + WTSClientProductId = 12, + WTSClientHardwareId = 13, + WTSClientAddress = 14, + WTSClientDisplay = 15, + WTSClientProtocolType = 16, + WTSIdleTime = 17, + WTSLogonTime = 18, + WTSIncomingBytes = 19, + WTSOutgoingBytes = 20, + WTSIncomingFrames = 21, + WTSOutgoingFrames = 22, + WTSClientInfo = 23, + WTSSessionInfo = 24, + WTSSessionInfoEx = 25, + WTSConfigInfo = 26, + WTSValidationInfo = 27, + WTSSessionAddressV4 = 28, + WTSIsRemoteSession = 29 + } + + public enum WTS_CONNECTSTATE_CLASS + { + WTSActive, + WTSConnected, + WTSConnectQuery, + WTSShadow, + WTSDisconnected, + WTSIdle, + WTSListen, + WTSReset, + WTSDown, + WTSInit + } + + private const int WTS_CURRENT_SERVER_HANDLE = 0; + + #endregion + + /// + /// Represents a user session + /// + public class UserSession + { + public int SessionId { get; set; } + public string UserName { get; set; } + public string DomainName { get; set; } + public string StationName { get; set; } + public WTS_CONNECTSTATE_CLASS State { get; set; } + public bool IsActiveConsoleSession { get; set; } + public bool IsRemoteSession { get; set; } + + public override string ToString() + { + return $"{DomainName}\\{UserName} (Session: {SessionId}, State: {State}, Console: {IsActiveConsoleSession}, Remote: {IsRemoteSession})"; + } + } + + /// + /// Gets all user sessions + /// + /// A list of user sessions + public List GetUserSessions() + { + var sessions = new List(); + IntPtr ppSessionInfo = IntPtr.Zero; + int count = 0; + int activeConsoleSessionId = (int)WTSGetActiveConsoleSessionId(); + + try + { + if (WTSEnumerateSessions(IntPtr.Zero, 0, 1, ref ppSessionInfo, ref count)) + { + IntPtr current = ppSessionInfo; + + for (int i = 0; i < count; i++) + { + WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure(current, typeof(WTS_SESSION_INFO)); + current = IntPtr.Add(current, Marshal.SizeOf(typeof(WTS_SESSION_INFO))); + + // Skip non-active sessions + if (si.State != WTS_CONNECTSTATE_CLASS.WTSActive) + continue; + + string userName = GetSessionInfo(si.SessionId, WTS_INFO_CLASS.WTSUserName); + string domainName = GetSessionInfo(si.SessionId, WTS_INFO_CLASS.WTSDomainName); + bool isRemoteSession = GetIsRemoteSession(si.SessionId); + + // Skip sessions without a user name (system sessions) + if (string.IsNullOrEmpty(userName)) + continue; + + sessions.Add(new UserSession + { + SessionId = si.SessionId, + UserName = userName, + DomainName = domainName, + StationName = si.pWinStationName, + State = si.State, + IsActiveConsoleSession = si.SessionId == activeConsoleSessionId, + IsRemoteSession = isRemoteSession + }); + } + } + } + finally + { + if (ppSessionInfo != IntPtr.Zero) + WTSFreeMemory(ppSessionInfo); + } + + return sessions; + } + + /// + /// Gets all interactive user sessions (console or RDP) + /// + /// A list of interactive user sessions + public List GetInteractiveUserSessions() + { + return GetUserSessions().Where(s => s.State == WTS_CONNECTSTATE_CLASS.WTSActive).ToList(); + } + + /// + /// Gets the active console session, if any + /// + /// The active console session, or null if none exists + public UserSession GetActiveConsoleSession() + { + return GetUserSessions().FirstOrDefault(s => s.IsActiveConsoleSession); + } + + /// + /// Gets the session information for the specified session ID and info class + /// + /// The session ID + /// The information class to retrieve + /// The session information + private string GetSessionInfo(int sessionId, WTS_INFO_CLASS infoClass) + { + IntPtr ppBuffer = IntPtr.Zero; + int bytesReturned = 0; + string result = string.Empty; + + try + { + if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, infoClass, out ppBuffer, out bytesReturned) && ppBuffer != IntPtr.Zero) + { + result = Marshal.PtrToStringAnsi(ppBuffer); + } + } + finally + { + if (ppBuffer != IntPtr.Zero) + WTSFreeMemory(ppBuffer); + } + + return result; + } + + /// + /// Determines if the specified session is a remote session + /// + /// The session ID + /// True if the session is remote, false otherwise + private bool GetIsRemoteSession(int sessionId) + { + IntPtr ppBuffer = IntPtr.Zero; + int bytesReturned = 0; + bool result = false; + + try + { + if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WTS_INFO_CLASS.WTSIsRemoteSession, out ppBuffer, out bytesReturned) && ppBuffer != IntPtr.Zero) + { + result = Marshal.ReadInt32(ppBuffer) != 0; + } + } + finally + { + if (ppBuffer != IntPtr.Zero) + WTSFreeMemory(ppBuffer); + } + + return result; + } + + /// + /// Gets the current interactive user information + /// + /// A UserSession object representing the current interactive user, or null if none exists + public UserSession GetCurrentInteractiveUser() + { + try + { + // First try to get the active console session + var consoleSession = GetActiveConsoleSession(); + if (consoleSession != null && !string.IsNullOrEmpty(consoleSession.UserName)) + { + return consoleSession; + } + + // If no active console session, try to find any active session + var activeSessions = GetInteractiveUserSessions(); + if (activeSessions.Count > 0) + { + return activeSessions[0]; + } + + // If no active sessions, try to get the current user + using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) + { + if (identity != null && !identity.IsSystem && !identity.IsAnonymous) + { + string[] parts = identity.Name.Split('\\'); + string domain = parts.Length > 1 ? parts[0] : ""; + string username = parts.Length > 1 ? parts[1] : parts[0]; + + return new UserSession + { + SessionId = -1, // Unknown session ID + UserName = username, + DomainName = domain, + StationName = "Unknown", + State = WTS_CONNECTSTATE_CLASS.WTSActive, + IsActiveConsoleSession = false, + IsRemoteSession = false + }; + } + } + + // If all else fails, try to get the user from the process owner + Process currentProcess = Process.GetCurrentProcess(); + string processOwner = GetProcessOwner(currentProcess.Id); + if (!string.IsNullOrEmpty(processOwner)) + { + string[] parts = processOwner.Split('\\'); + string domain = parts.Length > 1 ? parts[0] : ""; + string username = parts.Length > 1 ? parts[1] : parts[0]; + + return new UserSession + { + SessionId = -1, // Unknown session ID + UserName = username, + DomainName = domain, + StationName = "Unknown", + State = WTS_CONNECTSTATE_CLASS.WTSActive, + IsActiveConsoleSession = false, + IsRemoteSession = false + }; + } + + return null; + } + catch (Exception ex) + { + Debug.WriteLine($"Error getting current interactive user: {ex.Message}"); + return null; + } + } + + /// + /// Gets the owner of a process + /// + /// The process ID + /// The process owner in the format DOMAIN\Username + private string GetProcessOwner(int processId) + { + try + { + Process process = Process.GetProcessById(processId); + using (var searcher = new System.Management.ManagementObjectSearcher($"SELECT * FROM Win32_Process WHERE ProcessId = {processId}")) + { + foreach (var obj in searcher.Get()) + { + string[] args = new string[2]; + obj.InvokeMethod("GetOwner", args); + if (!string.IsNullOrEmpty(args[0])) + { + return $"{args[1]}\\{args[0]}"; // DOMAIN\Username + } + } + } + return string.Empty; + } + catch + { + return string.Empty; + } + } + } +} diff --git a/WindowsNotifications/Utils/AssemblyLoader.cs b/WindowsNotifications/Utils/AssemblyLoader.cs new file mode 100644 index 0000000..d2b1d80 --- /dev/null +++ b/WindowsNotifications/Utils/AssemblyLoader.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Reflection; + +namespace WindowsNotifications.Utils +{ + /// + /// Provides utilities for loading assemblies in PowerShell + /// + public static class AssemblyLoader + { + /// + /// Gets the current assembly as a byte array + /// + /// The assembly as a byte array + public static byte[] GetAssemblyBytes() + { + try + { + string assemblyPath = Assembly.GetExecutingAssembly().Location; + return File.ReadAllBytes(assemblyPath); + } + catch (Exception ex) + { + throw new Exception("Failed to get assembly bytes", ex); + } + } + + /// + /// Gets the current assembly as a Base64 encoded string + /// + /// The assembly as a Base64 encoded string + public static string GetAssemblyBase64() + { + try + { + byte[] bytes = GetAssemblyBytes(); + return Convert.ToBase64String(bytes); + } + catch (Exception ex) + { + throw new Exception("Failed to get assembly as Base64", ex); + } + } + + /// + /// Gets a PowerShell script for loading the assembly + /// + /// A PowerShell script for loading the assembly + public static string GetPowerShellLoaderScript() + { + return @" +function Load-WindowsNotifications { + [CmdletBinding()] + param ( + [Parameter(Mandatory=$false)] + [string]$AssemblyPath + ) + + try { + if ($AssemblyPath -and (Test-Path $AssemblyPath)) { + # Load from file + $bytes = [System.IO.File]::ReadAllBytes($AssemblyPath) + $assembly = [System.Reflection.Assembly]::Load($bytes) + } + else { + # Load from embedded Base64 string + $base64 = 'ASSEMBLY_BASE64_PLACEHOLDER' + $bytes = [System.Convert]::FromBase64String($base64) + $assembly = [System.Reflection.Assembly]::Load($bytes) + } + + # Return the assembly + return $assembly + } + catch { + Write-Error ""Failed to load WindowsNotifications assembly: $_"" + return $null + } +} + +# Example usage: +# $assembly = Load-WindowsNotifications +# $notificationManager = New-Object WindowsNotifications.NotificationManager +# $options = New-Object WindowsNotifications.Models.NotificationOptions +# $options.Title = ""Test Notification"" +# $options.Message = ""This is a test notification"" +# $result = $notificationManager.ShowNotification($options) +"; + } + } +} diff --git a/WindowsNotifications/Utils/LiteDBEmbedded.cs b/WindowsNotifications/Utils/LiteDBEmbedded.cs new file mode 100644 index 0000000..f8803ae --- /dev/null +++ b/WindowsNotifications/Utils/LiteDBEmbedded.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using WindowsNotifications.Models; + +namespace WindowsNotifications.Utils +{ + /// + /// Provides embedded LiteDB functionality + /// + internal class LiteDBEmbedded + { + private readonly string _dbPath; + private readonly Assembly _liteDbAssembly; + private readonly Type _liteDatabase; + private readonly Type _bsonMapper; + private readonly Type _bsonDocument; + private readonly Type _query; + private readonly Type _bsonValue; + private readonly object _db; + private readonly object _mapper; + + /// + /// Creates a new LiteDBEmbedded instance with the specified database path + /// + /// The path to the database file + public LiteDBEmbedded(string dbPath) + { + _dbPath = dbPath; + + // Load the embedded LiteDB assembly + _liteDbAssembly = LoadLiteDbAssembly(); + + // Get the types we need + _liteDatabase = _liteDbAssembly.GetType("LiteDB.LiteDatabase"); + _bsonMapper = _liteDbAssembly.GetType("LiteDB.BsonMapper"); + _bsonDocument = _liteDbAssembly.GetType("LiteDB.BsonDocument"); + _query = _liteDbAssembly.GetType("LiteDB.Query"); + _bsonValue = _liteDbAssembly.GetType("LiteDB.BsonValue"); + + // Create the mapper + _mapper = _bsonMapper.GetProperty("Global", BindingFlags.Public | BindingFlags.Static).GetValue(null); + + // Create the database + _db = Activator.CreateInstance(_liteDatabase, new object[] { _dbPath }); + + // Register the NotificationResult type + RegisterNotificationResultType(); + } + + /// + /// Loads the embedded LiteDB assembly + /// + /// The LiteDB assembly + private Assembly LoadLiteDbAssembly() + { + try + { + // Try to load from embedded resource + string resourceName = "WindowsNotifications.Resources.LiteDB.dll"; + using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) + { + if (stream != null) + { + byte[] assemblyData = new byte[stream.Length]; + stream.Read(assemblyData, 0, assemblyData.Length); + return Assembly.Load(assemblyData); + } + } + + // If not found as embedded resource, try to load from file + return Assembly.LoadFrom(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "LiteDB.dll")); + } + catch (Exception ex) + { + throw new Exception("Failed to load LiteDB assembly", ex); + } + } + + /// + /// Registers the NotificationResult type with the BsonMapper + /// + private void RegisterNotificationResultType() + { + try + { + // Get the Entity method + MethodInfo entityMethod = _bsonMapper.GetMethod("Entity", new Type[] { }); + + // Create a generic Entity method + MethodInfo genericEntityMethod = entityMethod.MakeGenericMethod(typeof(NotificationResult)); + + // Call the method to register the type + genericEntityMethod.Invoke(_mapper, null); + } + catch (Exception ex) + { + throw new Exception("Failed to register NotificationResult type", ex); + } + } + + /// + /// Inserts or updates a notification result in the database + /// + /// The notification result to save + /// True if the operation was successful, false otherwise + public bool UpsertNotificationResult(NotificationResult result) + { + try + { + // Get the collection + object collection = _liteDatabase.GetMethod("GetCollection", new Type[] { typeof(string) }) + .Invoke(_db, new object[] { "NotificationResults" }); + + // Convert the result to a BsonDocument + object bsonDoc = _bsonMapper.GetMethod("ToDocument", new Type[] { typeof(object) }) + .Invoke(_mapper, new object[] { result }); + + // Upsert the document + bool success = (bool)collection.GetType().GetMethod("Upsert", new Type[] { _bsonDocument }) + .Invoke(collection, new object[] { bsonDoc }); + + return success; + } + catch (Exception) + { + return false; + } + } + + /// + /// Gets a notification result from the database + /// + /// The ID of the notification + /// The notification result, or null if not found + public NotificationResult GetNotificationResult(string notificationId) + { + try + { + // Get the collection + object collection = _liteDatabase.GetMethod("GetCollection", new Type[] { typeof(string) }) + .Invoke(_db, new object[] { "NotificationResults" }); + + // Create a query for the notification ID + object query = collection.GetType().GetMethod("FindById") + .Invoke(collection, new object[] { CreateBsonValue(notificationId) }); + + // Convert the result to a NotificationResult + if (query != null) + { + return (NotificationResult)_bsonMapper.GetMethod("ToObject", new Type[] { typeof(Type), _bsonDocument }) + .Invoke(_mapper, new object[] { typeof(NotificationResult), query }); + } + + return null; + } + catch (Exception) + { + return null; + } + } + + /// + /// Gets all notification results from the database + /// + /// A list of notification results + public List GetAllNotificationResults() + { + try + { + // Get the collection + object collection = _liteDatabase.GetMethod("GetCollection", new Type[] { typeof(string) }) + .Invoke(_db, new object[] { "NotificationResults" }); + + // Find all documents + object query = collection.GetType().GetMethod("FindAll") + .Invoke(collection, null); + + // Convert the results to a list of NotificationResults + var results = new List(); + + // Enumerate the results + foreach (object doc in (System.Collections.IEnumerable)query) + { + var result = (NotificationResult)_bsonMapper.GetMethod("ToObject", new Type[] { typeof(Type), _bsonDocument }) + .Invoke(_mapper, new object[] { typeof(NotificationResult), doc }); + + results.Add(result); + } + + return results; + } + catch (Exception) + { + return new List(); + } + } + + /// + /// Deletes a notification result from the database + /// + /// The ID of the notification + /// True if the deletion was successful, false otherwise + public bool DeleteNotificationResult(string notificationId) + { + try + { + // Get the collection + object collection = _liteDatabase.GetMethod("GetCollection", new Type[] { typeof(string) }) + .Invoke(_db, new object[] { "NotificationResults" }); + + // Delete the document + bool success = (bool)collection.GetType().GetMethod("Delete", new Type[] { _bsonValue }) + .Invoke(collection, new object[] { CreateBsonValue(notificationId) }); + + return success; + } + catch (Exception) + { + return false; + } + } + + /// + /// Deletes all notification results from the database + /// + /// True if the deletion was successful, false otherwise + public bool DeleteAllNotificationResults() + { + try + { + // Get the collection + object collection = _liteDatabase.GetMethod("GetCollection", new Type[] { typeof(string) }) + .Invoke(_db, new object[] { "NotificationResults" }); + + // Delete all documents + int count = (int)collection.GetType().GetMethod("DeleteAll") + .Invoke(collection, null); + + return count > 0; + } + catch (Exception) + { + return false; + } + } + + /// + /// Creates a BsonValue from a string + /// + /// The string value + /// A BsonValue object + private object CreateBsonValue(string value) + { + return Activator.CreateInstance(_bsonValue, new object[] { value }); + } + } +} diff --git a/WindowsNotifications/WindowsNotifications.csproj b/WindowsNotifications/WindowsNotifications.csproj new file mode 100644 index 0000000..97df255 --- /dev/null +++ b/WindowsNotifications/WindowsNotifications.csproj @@ -0,0 +1,70 @@ + + + + + Debug + AnyCPU + {A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6} + Library + Properties + WindowsNotifications + WindowsNotifications + v4.7.2 + 512 + true + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + False + $(MSBuildProgramFiles32)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\System.Runtime.WindowsRuntime.dll + True + + + $(MSBuildProgramFiles32)\Windows Kits\10\UnionMetadata\10.0.19041.0\Windows.winmd + False + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 0000000..b8d015d --- /dev/null +++ b/build.ps1 @@ -0,0 +1,46 @@ +# Build script for WindowsNotifications + +# Set the configuration +$Configuration = "Release" + +# Find MSBuild +$msbuildPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe" +if (-not (Test-Path $msbuildPath)) { + $msbuildPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\MSBuild.exe" +} +if (-not (Test-Path $msbuildPath)) { + $msbuildPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\MSBuild.exe" +} +if (-not (Test-Path $msbuildPath)) { + $msbuildPath = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\MSBuild.exe" +} +if (-not (Test-Path $msbuildPath)) { + # Try to find MSBuild in the PATH + $msbuildPath = "MSBuild.exe" +} + +# Clean the solution +Write-Host "Cleaning solution..." -ForegroundColor Cyan +& $msbuildPath WindowsNotifications.sln /t:Clean /p:Configuration=$Configuration + +# Restore NuGet packages +Write-Host "Restoring NuGet packages..." -ForegroundColor Cyan +& $msbuildPath WindowsNotifications.sln /t:Restore /p:Configuration=$Configuration + +# Build the solution +Write-Host "Building solution..." -ForegroundColor Cyan +& $msbuildPath WindowsNotifications.sln /t:Build /p:Configuration=$Configuration + +# Check if the build was successful +if ($LASTEXITCODE -eq 0) { + Write-Host "Build completed successfully!" -ForegroundColor Green + + # Show the output directory + $outputDir = Join-Path -Path $PSScriptRoot -ChildPath "WindowsNotifications\bin\$Configuration" + Write-Host "Output directory: $outputDir" -ForegroundColor Yellow + + # List the files in the output directory + Get-ChildItem -Path $outputDir | Format-Table Name, Length +} else { + Write-Host "Build failed with exit code $LASTEXITCODE" -ForegroundColor Red +}