From 7cd5962115c7df9914ea9d62522e0f85dfa8eb6f Mon Sep 17 00:00:00 2001 From: GraceSolutions Date: Thu, 10 Apr 2025 21:13:24 -0400 Subject: [PATCH] Add PowerShell cmdlets and update project structure --- Examples/AsyncNotification.ps1 | 94 +- Examples/CountdownAndDeadline.ps1 | 145 +-- Examples/CustomBrandedNotification.ps1 | 105 +-- Examples/LoadFromBase64.ps1 | 56 +- Examples/NotificationWithButtons.ps1 | 59 +- Examples/PowerShellModule.ps1 | 111 +-- Examples/PowerShellModuleNew.ps1 | 42 + Examples/README.md | 10 + Examples/RebootNotification.ps1 | 74 +- Examples/SimpleNotification.ps1 | 49 +- NuGet.config | 6 + README.md | 277 +----- SimpleWindowsNotifications.sln | 30 - .../NotificationManagerTests.cs | 235 +++-- .../NotificationOptionsTests.cs | 149 +--- .../NotificationResultTests.cs | 100 ++- .../Properties/AssemblyInfo.cs | 36 + WindowsNotifications.Tests/UnitTest1.cs | 18 + .../WindowsNotifications.Tests.csproj | 77 +- WindowsNotifications.Tests/packages.config | 5 + WindowsNotifications.sln | 16 +- .../ClearAllNotificationResultsCmdlet.cs | 42 + .../GetAllNotificationResultsCmdlet.cs | 42 + .../GetInteractiveUserSessionsCmdlet.cs | 42 + .../Cmdlets/GetNotificationResultCmdlet.cs | 48 + .../InitializeWindowsNotificationsCmdlet.cs | 54 ++ .../Cmdlets/RemoveNotificationResultCmdlet.cs | 48 + .../Cmdlets/ShowNotificationCmdlet.cs | 123 +++ .../Cmdlets/TestSystemContextCmdlet.cs | 42 + .../Cmdlets/WaitNotificationCmdlet.cs | 60 ++ WindowsNotifications/Models/DeadlineAction.cs | 132 +-- .../Models/DeferralOptions.cs | 95 +- .../Models/NotificationButton.cs | 64 ++ .../Models/NotificationOptions.cs | 227 +---- .../Models/NotificationResult.cs | 64 +- .../Models/SimpleNotificationOptions.cs | 116 --- .../Models/SimpleNotificationResult.cs | 84 -- WindowsNotifications/NotificationManager.cs | 410 ++++----- .../Properties/AssemblyInfo.cs | 4 +- WindowsNotifications/Resources/LiteDB.dll | Bin 355328 -> 123 bytes .../Services/DatabaseService.cs | 128 +-- .../Services/ToastNotificationService.cs | 838 ++++++------------ .../Services/UserImpersonation.cs | 511 ++++++----- .../Services/UserSessionManager.cs | 489 ++++------ .../SimpleNotificationManager.cs | 228 ----- .../SimpleWindowsNotifications.csproj | 47 - WindowsNotifications/Utils/AssemblyLoader.cs | 99 +-- WindowsNotifications/Utils/LiteDBEmbedded.cs | 257 +----- .../WindowsNotifications.csproj | 44 +- build.ps1 | 98 +- check-assembly.ps1 | 3 + docs/README.md | 9 + run-tests.ps1 | 33 - simple-build.ps1 | 37 - src/README.md | 8 + test-simple.ps1 | 50 -- 56 files changed, 2356 insertions(+), 3914 deletions(-) create mode 100644 Examples/PowerShellModuleNew.ps1 create mode 100644 Examples/README.md create mode 100644 NuGet.config delete mode 100644 SimpleWindowsNotifications.sln create mode 100644 WindowsNotifications.Tests/Properties/AssemblyInfo.cs create mode 100644 WindowsNotifications.Tests/UnitTest1.cs create mode 100644 WindowsNotifications.Tests/packages.config create mode 100644 WindowsNotifications/Cmdlets/ClearAllNotificationResultsCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/GetAllNotificationResultsCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/GetInteractiveUserSessionsCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/GetNotificationResultCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/InitializeWindowsNotificationsCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/RemoveNotificationResultCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/ShowNotificationCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/TestSystemContextCmdlet.cs create mode 100644 WindowsNotifications/Cmdlets/WaitNotificationCmdlet.cs create mode 100644 WindowsNotifications/Models/NotificationButton.cs delete mode 100644 WindowsNotifications/Models/SimpleNotificationOptions.cs delete mode 100644 WindowsNotifications/Models/SimpleNotificationResult.cs delete mode 100644 WindowsNotifications/SimpleNotificationManager.cs delete mode 100644 WindowsNotifications/SimpleWindowsNotifications.csproj create mode 100644 check-assembly.ps1 create mode 100644 docs/README.md delete mode 100644 run-tests.ps1 delete mode 100644 simple-build.ps1 create mode 100644 src/README.md delete mode 100644 test-simple.ps1 diff --git a/Examples/AsyncNotification.ps1 b/Examples/AsyncNotification.ps1 index 2d82f64..a0a4764 100644 --- a/Examples/AsyncNotification.ps1 +++ b/Examples/AsyncNotification.ps1 @@ -1,84 +1,60 @@ # Example: Asynchronous Notification -# This script demonstrates how to show an asynchronous notification and check its status later +# This script demonstrates how to show an asynchronous notification -# Load the WindowsNotifications assembly +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force + +# Initialize the notification system $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 -} +Initialize-WindowsNotifications -DllPath $dllPath -# Create a notification manager -$notificationManager = New-Object WindowsNotifications.NotificationManager +# Show an asynchronous notification +$result = Show-Notification -Title "Background Task" -Message "A background task is running..." -Buttons "Cancel", "View Details" -Async -# 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 +# Display the initial result Write-Host "Notification displayed: $($result.Displayed)" Write-Host "Notification ID: $($result.NotificationId)" -# Simulate doing some work -Write-Host "Performing background work..." +# Simulate a background task +Write-Host "Performing background task..." for ($i = 1; $i -le 5; $i++) { - Write-Host " Working... ($i/5)" + Write-Host "Working... ($i/5)" Start-Sleep -Seconds 2 # Check if the notification has been interacted with - $currentResult = $notificationManager.GetNotificationResult($result.NotificationId) + $currentResult = Get-NotificationResult -NotificationId $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!" + if ($currentResult -and ($currentResult.Activated -or $currentResult.Dismissed)) { + Write-Host "User interacted with the notification" + + if ($currentResult.ClickedButtonId) { + Write-Host "Button clicked: $($currentResult.ClickedButtonText) (ID: $($currentResult.ClickedButtonId))" - # 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) + # Handle button clicks + if ($currentResult.ClickedButtonText -eq "Cancel") { + Write-Host "User cancelled the task" + break + } elseif ($currentResult.ClickedButtonText -eq "View Details") { + Write-Host "User wants to view details" + # In a real script, you would show details here + } } + + break } } -Write-Host "Background work completed" +Write-Host "Background task 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) { +# Get the final result +$finalResult = Get-NotificationResult -NotificationId $result.NotificationId +if ($finalResult) { Write-Host "Final notification state:" Write-Host " Activated: $($finalResult.Activated)" Write-Host " Dismissed: $($finalResult.Dismissed)" + if ($finalResult.ClickedButtonId) { - Write-Host " Button clicked: $($finalResult.ClickedButtonText)" + Write-Host " Button clicked: $($finalResult.ClickedButtonText) (ID: $($finalResult.ClickedButtonId))" } } diff --git a/Examples/CountdownAndDeadline.ps1 b/Examples/CountdownAndDeadline.ps1 index a9a961b..72e493d 100644 --- a/Examples/CountdownAndDeadline.ps1 +++ b/Examples/CountdownAndDeadline.ps1 @@ -1,125 +1,44 @@ # Example: Countdown and Deadline Notification -# This script demonstrates how to create a notification with a countdown timer and deadline action +# This script demonstrates how to show a notification with a countdown timer and deadline -# Load the WindowsNotifications assembly +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force + +# Initialize the notification system $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 -} +Initialize-WindowsNotifications -DllPath $dllPath -# Create a notification manager -$notificationManager = New-Object WindowsNotifications.NotificationManager +# Set a deadline 2 minutes from now +$deadline = (Get-Date).AddMinutes(2) -# 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) +# Show a countdown notification +$result = Show-Notification -Title "System Maintenance Required" -Message "Your system needs to restart for maintenance. Please save your work." -DeadlineTime $deadline -ShowCountdown -Buttons "Restart Now", "Remind Me Later" # 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)" +# If the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" + if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText) (ID: $($result.ClickedButtonId))" + + # Handle button clicks + if ($result.ClickedButtonText -eq "Restart Now") { + Write-Host "User chose to restart now (simulated)" + # In a real script, you would restart the computer here + # Restart-Computer -Force + } elseif ($result.ClickedButtonText -eq "Remind Me Later") { + Write-Host "User chose to be reminded later" + # In a real script, you would schedule a reminder here + } } +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" +} elseif ($result.DeadlineReached) { + Write-Host "Deadline was reached at $($result.DeadlineReachedTime)" + Write-Host "Taking automatic action (simulated)" + # In a real script, you would take the automatic action here } - -Write-Host "`nLog file: $logFile" diff --git a/Examples/CustomBrandedNotification.ps1 b/Examples/CustomBrandedNotification.ps1 index db4a09a..b3e4534 100644 --- a/Examples/CustomBrandedNotification.ps1 +++ b/Examples/CustomBrandedNotification.ps1 @@ -1,105 +1,62 @@ # Example: Custom Branded Notification -# This script demonstrates how to create a notification with custom branding +# This script demonstrates how to show a notification with custom branding -# Load the WindowsNotifications assembly +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force + +# Initialize the notification system $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 -} +Initialize-WindowsNotifications -DllPath $dllPath -# Create a notification manager -$notificationManager = New-Object WindowsNotifications.NotificationManager - -# Create custom notification options with branding +# Create custom notification options $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." +$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 +$options.Attribution = "Contoso IT Department" -# Set custom images -# Note: Replace these paths with actual image paths on your system -$logoPath = Join-Path -Path $PSScriptRoot -ChildPath "Images\company-logo.png" +# Set custom images (file paths or URLs) +$logoPath = Join-Path -Path $PSScriptRoot -ChildPath "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" +$heroPath = Join-Path -Path $PSScriptRoot -ChildPath "banner.png" if (Test-Path $heroPath) { $options.HeroImagePath = $heroPath } -# Add custom buttons with images +# Add custom buttons $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 +$laterButton = New-Object WindowsNotifications.Models.NotificationButton("Later", "later") $options.Buttons.Add($updateButton) -$options.Buttons.Add($deferButton) -$options.Buttons.Add($moreInfoButton) - -# Configure audio -$options.AudioSource = "ms-winsoundevent:Notification.Default" -$options.SilentMode = $false +$options.Buttons.Add($laterButton) # Show the notification -Write-Host "Showing custom branded notification..." +$notificationManager = New-Object WindowsNotifications.NotificationManager $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 the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" + if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText) (ID: $($result.ClickedButtonId))" - 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 - } - } + # Handle button clicks + if ($result.ClickedButtonId -eq "install") { + Write-Host "User chose to install the update (simulated)" + # In a real script, you would install the update here + } elseif ($result.ClickedButtonId -eq "later") { + Write-Host "User chose to install later" + # In a real script, you would schedule a reminder here } } +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" } diff --git a/Examples/LoadFromBase64.ps1 b/Examples/LoadFromBase64.ps1 index be3ad60..170f4b7 100644 --- a/Examples/LoadFromBase64.ps1 +++ b/Examples/LoadFromBase64.ps1 @@ -1,35 +1,29 @@ -# Example: Load Assembly from Base64 +# Example: Load 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 +# First, convert the DLL to a Base64 string +# In a real scenario, you would have this string pre-generated $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." +$bytes = [System.IO.File]::ReadAllBytes($dllPath) +$base64 = [Convert]::ToBase64String($bytes) + +# Now, load the assembly from the Base64 string +$assemblyBytes = [Convert]::FromBase64String($base64) +$assembly = [System.Reflection.Assembly]::Load($assemblyBytes) + +# Create a notification manager +$notificationManager = New-Object WindowsNotifications.NotificationManager + +# Show a simple notification +$result = $notificationManager.ShowSimpleNotification("Loaded from Base64", "This notification was shown using an assembly loaded from a Base64 string.") + +# Display the result +Write-Host "Notification displayed: $($result.Displayed)" +Write-Host "Notification ID: $($result.NotificationId)" + +# If the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" } diff --git a/Examples/NotificationWithButtons.ps1 b/Examples/NotificationWithButtons.ps1 index ad0ad1f..b0a5bae 100644 --- a/Examples/NotificationWithButtons.ps1 +++ b/Examples/NotificationWithButtons.ps1 @@ -1,58 +1,27 @@ # 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 -} +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force -# Create a notification manager -$notificationManager = New-Object WindowsNotifications.NotificationManager +# Initialize the notification system +$dllPath = Join-Path -Path $PSScriptRoot -ChildPath "..\WindowsNotifications\bin\Release\WindowsNotifications.dll" +Initialize-WindowsNotifications -DllPath $dllPath # Show a notification with buttons -Write-Host "Showing notification with buttons..." -$result = $notificationManager.ShowNotificationWithButtons( - "Action Required", - "Please select an option below:", - "Approve", - "Reject", - "Defer" -) +$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 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" - } - } - } +# If the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" + if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText) (ID: $($result.ClickedButtonId))" } +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" } diff --git a/Examples/PowerShellModule.ps1 b/Examples/PowerShellModule.ps1 index 9a3959c..354c1c1 100644 --- a/Examples/PowerShellModule.ps1 +++ b/Examples/PowerShellModule.ps1 @@ -1,21 +1,13 @@ -# Example: Using the WindowsNotifications PowerShell Module -# This script demonstrates how to use the WindowsNotifications PowerShell module +# Example: PowerShell Module +# This script demonstrates how to use the PowerShell module # Import the module -$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell" +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" Import-Module $modulePath -Force -# Initialize the module -# Note: The DLL must be in the same directory as the module files +# Initialize the notification system $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 -} +Initialize-WindowsNotifications -DllPath $dllPath # Check if running as SYSTEM $isSystem = Test-SystemContext @@ -23,94 +15,29 @@ Write-Host "Running as SYSTEM: $isSystem" # Get interactive user sessions $sessions = Get-InteractiveUserSessions -Write-Host "Interactive user sessions found: $($sessions.Count)" +Write-Host "Interactive user sessions: $($sessions.Count)" foreach ($session in $sessions) { - Write-Host " $session" + 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" +$result = Show-Notification -Title "PowerShell Module" -Message "This notification was shown using 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" } - } +# If the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" } -# 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" +# Get all notification results +$results = Get-AllNotificationResults +Write-Host "Total notification results: $($results.Count)" -# 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" +# Clear all notification results +Clear-AllNotificationResults +Write-Host "All notification results cleared" diff --git a/Examples/PowerShellModuleNew.ps1 b/Examples/PowerShellModuleNew.ps1 new file mode 100644 index 0000000..3c6ff77 --- /dev/null +++ b/Examples/PowerShellModuleNew.ps1 @@ -0,0 +1,42 @@ +# Example: PowerShell Module +# This script demonstrates how to use the PowerShell module + +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force + +# Initialize the notification system +Initialize-WindowsNotifications + +# Check if running as SYSTEM +$isSystem = Test-SystemContext +Write-Output "Running as SYSTEM: $isSystem" + +# Get interactive user sessions +$sessions = Get-InteractiveUserSessions +Write-Output "Interactive user sessions: $($sessions.Count)" +foreach ($session in $sessions) { + Write-Output " - $session" +} + +# Show a simple notification +$result = Show-Notification -Title "PowerShell Module" -Message "This notification was shown using the PowerShell module." + +# Display the result +Write-Output "Notification displayed: $($result.Displayed)" +Write-Output "Notification ID: $($result.NotificationId)" + +# If the notification was interacted with, show the details +if ($result.Activated) { + Write-Output "Notification was activated" +} elseif ($result.Dismissed) { + Write-Output "Notification was dismissed" +} + +# Get all notification results +$results = Get-AllNotificationResults +Write-Output "Total notification results: $($results.Count)" + +# Clear all notification results +Clear-AllNotificationResults +Write-Output "All notification results cleared" diff --git a/Examples/README.md b/Examples/README.md new file mode 100644 index 0000000..227d941 --- /dev/null +++ b/Examples/README.md @@ -0,0 +1,10 @@ +# Examples + +This directory contains example scripts demonstrating how to use the Windows Notifications system. + +## Examples + +- `SimpleNotification.ps1` - Shows a simple notification +- `NotificationWithButtons.ps1` - Shows a notification with buttons +- `CountdownNotification.ps1` - Shows a notification with a countdown timer +- `SystemContextNotification.ps1` - Shows a notification from SYSTEM context diff --git a/Examples/RebootNotification.ps1 b/Examples/RebootNotification.ps1 index af81cc9..1ec624d 100644 --- a/Examples/RebootNotification.ps1 +++ b/Examples/RebootNotification.ps1 @@ -1,68 +1,36 @@ -# Example: Reboot Notification with Deferrals +# Example: Reboot Notification # This script demonstrates how to show a reboot notification with deferral options -# Load the WindowsNotifications assembly +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force + +# Initialize the notification system $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 -} +Initialize-WindowsNotifications -DllPath $dllPath -# 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) +# Show a 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 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 the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" + if ($result.ClickedButtonId) { + Write-Host "Button clicked: $($result.ClickedButtonText) (ID: $($result.ClickedButtonId))" + # Handle button clicks if ($result.ClickedButtonId -eq "reboot") { - Write-Host "User chose to reboot now" - # In a real script, you would initiate a reboot here + Write-Host "User chose to reboot now (simulated)" + # In a real script, you would reboot the computer here # Restart-Computer -Force } - elseif ($result.Deferred) { - Write-Host "User deferred the reboot until: $($result.DeferredUntil)" - Write-Host "Deferral reason: $($result.DeferralReason)" - } } +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" +} elseif ($result.Deferred) { + Write-Host "Notification was deferred until: $($result.DeferredUntil)" } diff --git a/Examples/SimpleNotification.ps1 b/Examples/SimpleNotification.ps1 index 17c0a22..1ca2577 100644 --- a/Examples/SimpleNotification.ps1 +++ b/Examples/SimpleNotification.ps1 @@ -1,49 +1,24 @@ # Example: Simple Notification # This script demonstrates how to show a simple notification -# Load the WindowsNotifications assembly +# Import the module +$modulePath = Join-Path -Path $PSScriptRoot -ChildPath "..\PowerShell\WindowsNotifications.psd1" +Import-Module $modulePath -Force + +# Initialize the notification system $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" -} +Initialize-WindowsNotifications -DllPath $dllPath # Show a simple notification -Write-Host "Showing simple notification..." -$result = $notificationManager.ShowSimpleNotification("Simple Notification", "This is a simple notification from PowerShell") +$result = Show-Notification -Title "Simple Notification" -Message "This is a simple notification." -TimeoutInSeconds 10 # 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" - } +# If the notification was interacted with, show the details +if ($result.Activated) { + Write-Host "Notification was activated" +} elseif ($result.Dismissed) { + Write-Host "Notification was dismissed" } diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 0000000..6873eb9 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md index 4d1096b..0db324a 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,26 @@ # 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. +A .NET DLL library for PowerShell 5 or 7 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 +- **Custom Branding**: Support for custom branding with logos, images, and attribution - **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 +- **PowerShell Integration**: Easily load and use the library in PowerShell 5 or 7 - **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 + +## Project Structure + +- `WindowsNotifications/` - Core .NET library +- `WindowsNotifications.Tests/` - Unit tests +- `PowerShell/` - PowerShell module +- `Examples/` - Example scripts ## Requirements @@ -83,257 +89,14 @@ if ($result.ClickedButtonId) { } ``` -#### Reboot Notification with Deferrals +### PowerShell Module + +The library includes a PowerShell module that makes it easier to use the Windows Notifications functionality in your PowerShell scripts. ```powershell -# Create a notification manager -$notificationManager = New-Object WindowsNotifications.NotificationManager +# Import the module +Import-Module WindowsNotifications -# 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 @@ -342,9 +105,6 @@ 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. @@ -365,10 +125,9 @@ See the [Examples](Examples) directory for complete PowerShell script examples: ## 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 +2. Run the build script: `./build.ps1 -Release` +3. The compiled DLL will be in the `WindowsNotifications\bin\Release` directory +4. The PowerShell module will be in the `PowerShell` directory ## License diff --git a/SimpleWindowsNotifications.sln b/SimpleWindowsNotifications.sln deleted file mode 100644 index 5c5e451..0000000 --- a/SimpleWindowsNotifications.sln +++ /dev/null @@ -1,30 +0,0 @@ -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}") = "SimpleWindowsNotifications", "WindowsNotifications\SimpleWindowsNotifications.csproj", "{A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsNotifications.Tests", "WindowsNotifications.Tests\WindowsNotifications.Tests.csproj", "{B1A2C3D4-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 - {B1A2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B1A2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B1A2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B1A2C3D4-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.Tests/NotificationManagerTests.cs b/WindowsNotifications.Tests/NotificationManagerTests.cs index 86c0dfd..bd6e120 100644 --- a/WindowsNotifications.Tests/NotificationManagerTests.cs +++ b/WindowsNotifications.Tests/NotificationManagerTests.cs @@ -1,6 +1,7 @@ using NUnit.Framework; using System; using System.IO; +using WindowsNotifications; using WindowsNotifications.Models; namespace WindowsNotifications.Tests @@ -8,203 +9,165 @@ namespace WindowsNotifications.Tests [TestFixture] public class NotificationManagerTests { - [Test] - public void Constructor_SetsDefaultDatabasePath() - { - // Act - var manager = new SimpleNotificationManager(); + private string _testDbPath; + private NotificationManager _manager; - // Assert - Assert.IsNotNull(manager.DatabasePath); - Assert.IsTrue(manager.DatabasePath.Contains("WindowsNotifications")); - Assert.IsTrue(manager.DatabasePath.EndsWith("notifications.db")); + [SetUp] + public void Setup() + { + _testDbPath = Path.Combine(Path.GetTempPath(), string.Format("test_notifications_{0}.db", Guid.NewGuid())); + _manager = new NotificationManager(_testDbPath); } - [Test] - public void Constructor_SetsCustomDatabasePath() + [TearDown] + public void TearDown() { - // Arrange - string customPath = Path.Combine(Path.GetTempPath(), "custom.db"); - - // Act - var manager = new SimpleNotificationManager(customPath); - - // Assert - Assert.AreEqual(customPath, manager.DatabasePath); - } - - [Test] - public void ShowNotification_ValidatesOptions() - { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act & Assert - Assert.Throws(() => manager.ShowNotification(null)); - - var emptyOptions = new NotificationOptions(); - Assert.Throws(() => manager.ShowNotification(emptyOptions)); - } - - [Test] - public void ShowNotification_ReturnsValidResult() - { - // Arrange - var manager = new SimpleNotificationManager(); - var options = new NotificationOptions + if (File.Exists(_testDbPath)) { - Title = "Test Title", - Message = "Test Message" - }; - - // Act - var result = manager.ShowNotification(options); - - // Assert - Assert.IsNotNull(result); - Assert.AreEqual(options.Id, result.NotificationId); - Assert.IsTrue(result.Displayed); - Assert.IsNotNull(result.CreatedTime); + try + { + File.Delete(_testDbPath); + } + catch + { + // Ignore errors + } + } } [Test] - public void ShowSimpleNotification_ReturnsValidResult() + public void NotificationManager_Constructor_DefaultPath_IsCorrect() { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - var result = manager.ShowSimpleNotification("Test Title", "Test Message"); + // Arrange & Act + var manager = new NotificationManager(); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.NotificationId); - Assert.IsTrue(result.Displayed); - Assert.IsNotNull(result.CreatedTime); + string dbPath = manager.GetDatabaseFilePath(); + Assert.IsNotNull(dbPath); + Assert.IsTrue(dbPath.Contains("WindowsNotifications")); + Assert.IsTrue(dbPath.EndsWith(".db")); } [Test] - public void ShowNotificationWithButtons_ReturnsValidResult() + public void NotificationManager_Constructor_CustomPath_IsCorrect() { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - var result = manager.ShowNotificationWithButtons("Test Title", "Test Message", "Button 1", "Button 2"); + // Arrange & Act + var customPath = Path.Combine(Path.GetTempPath(), "custom_notifications.db"); + var manager = new NotificationManager(customPath); // Assert - Assert.IsNotNull(result); - Assert.IsNotNull(result.NotificationId); - Assert.IsTrue(result.Displayed); - Assert.IsNotNull(result.CreatedTime); - Assert.IsTrue(result.Activated); - Assert.IsNotNull(result.ClickedButtonId); - Assert.IsNotNull(result.ClickedButtonText); - Assert.IsNotNull(result.ClickedButtonArgument); + Assert.AreEqual(customPath, manager.GetDatabaseFilePath()); } [Test] - public void GetDatabaseFilePath_ReturnsDatabasePath() + public void NotificationManager_IsRunningAsSystem_ReturnsCorrectValue() { - // Arrange - string customPath = Path.Combine(Path.GetTempPath(), "custom.db"); - var manager = new SimpleNotificationManager(customPath); - - // Act - string path = manager.GetDatabaseFilePath(); - - // Assert - Assert.AreEqual(customPath, path); - } - - [Test] - public void IsRunningAsSystem_ReturnsFalse() - { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - bool isSystem = manager.IsRunningAsSystem(); + // Arrange & Act + bool isSystem = _manager.IsRunningAsSystem(); // Assert + // This will be false in a test environment, but we're just testing the method returns a value Assert.IsFalse(isSystem); } [Test] - public void GetInteractiveUserSessions_ReturnsEmptyList() + public void NotificationManager_GetInteractiveUserSessions_ReturnsNonNullList() { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - var sessions = manager.GetInteractiveUserSessions(); + // Arrange & Act + var sessions = _manager.GetInteractiveUserSessions(); // Assert Assert.IsNotNull(sessions); - Assert.AreEqual(0, sessions.Count); + // We can't assert the count because it depends on the environment } [Test] - public void GetNotificationResult_ReturnsNull() + public void NotificationManager_ShowSimpleNotification_ReturnsResult() { // Arrange - var manager = new SimpleNotificationManager(); + string title = "Test Title"; + string message = "Test Message"; // Act - var result = manager.GetNotificationResult("test-id"); + var result = _manager.ShowSimpleNotification(title, message); + + // Assert + Assert.IsNotNull(result); + Assert.IsNotNull(result.NotificationId); + } + + [Test] + public void NotificationManager_ShowNotificationWithButtons_ReturnsResult() + { + // Arrange + string title = "Test Title"; + string message = "Test Message"; + string[] buttons = new[] { "OK", "Cancel" }; + + // Act + var result = _manager.ShowNotificationWithButtons(title, message, buttons); + + // Assert + Assert.IsNotNull(result); + Assert.IsNotNull(result.NotificationId); + } + + [Test] + public void NotificationManager_ShowRebootNotification_ReturnsResult() + { + // Arrange + string title = "Test Title"; + string message = "Test Message"; + + // Act + var result = _manager.ShowRebootNotification(title, message); + + // Assert + Assert.IsNotNull(result); + Assert.IsNotNull(result.NotificationId); + } + + [Test] + public void NotificationManager_GetNotificationResult_ReturnsNullForNonExistentId() + { + // Arrange + string nonExistentId = Guid.NewGuid().ToString(); + + // Act + var result = _manager.GetNotificationResult(nonExistentId); // Assert Assert.IsNull(result); } [Test] - public void WaitForNotification_ReturnsNull() + public void NotificationManager_GetAllNotificationResults_ReturnsNonNullList() { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - var result = manager.WaitForNotification("test-id", 100); - - // Assert - Assert.IsNull(result); - } - - [Test] - public void GetAllNotificationResults_ReturnsEmptyList() - { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - var results = manager.GetAllNotificationResults(); + // Arrange & Act + var results = _manager.GetAllNotificationResults(); // Assert Assert.IsNotNull(results); - Assert.AreEqual(0, results.Count); } [Test] - public void DeleteNotificationResult_ReturnsTrue() + public void NotificationManager_DeleteNotificationResult_ReturnsFalseForNonExistentId() { // Arrange - var manager = new SimpleNotificationManager(); + string nonExistentId = Guid.NewGuid().ToString(); // Act - bool result = manager.DeleteNotificationResult("test-id"); + bool result = _manager.DeleteNotificationResult(nonExistentId); // Assert - Assert.IsTrue(result); + Assert.IsFalse(result); } [Test] - public void DeleteAllNotificationResults_ReturnsTrue() + public void NotificationManager_DeleteAllNotificationResults_ReturnsTrue() { - // Arrange - var manager = new SimpleNotificationManager(); - - // Act - bool result = manager.DeleteAllNotificationResults(); + // Arrange & Act + bool result = _manager.DeleteAllNotificationResults(); // Assert Assert.IsTrue(result); diff --git a/WindowsNotifications.Tests/NotificationOptionsTests.cs b/WindowsNotifications.Tests/NotificationOptionsTests.cs index 3de7967..08722a8 100644 --- a/WindowsNotifications.Tests/NotificationOptionsTests.cs +++ b/WindowsNotifications.Tests/NotificationOptionsTests.cs @@ -1,6 +1,5 @@ using NUnit.Framework; using System; -using System.Linq; using WindowsNotifications.Models; namespace WindowsNotifications.Tests @@ -9,36 +8,41 @@ namespace WindowsNotifications.Tests public class NotificationOptionsTests { [Test] - public void Constructor_SetsDefaultValues() + public void NotificationOptions_DefaultValues_AreCorrect() { - // Act + // Arrange & Act var options = new NotificationOptions(); // Assert - Assert.IsNull(options.Title); - Assert.IsNull(options.Message); - Assert.IsNull(options.LogoImagePath); - Assert.IsNull(options.HeroImagePath); - Assert.IsNull(options.Attribution); - Assert.AreEqual(0, options.TimeoutInSeconds); + Assert.IsNotNull(options.Buttons); + Assert.AreEqual(0, options.Buttons.Count); Assert.IsFalse(options.Async); Assert.IsNotNull(options.Id); Assert.IsTrue(Guid.TryParse(options.Id, out _)); - Assert.IsNull(options.Tag); - Assert.IsNull(options.Group); - Assert.IsFalse(options.PersistState); - Assert.IsTrue(options.EnableLogging); + Assert.IsTrue(options.PersistState); + Assert.AreEqual(60, options.ReminderTimeInMinutes); + Assert.IsFalse(options.ShowReminder); + Assert.IsFalse(options.ShowCountdown); + Assert.IsFalse(options.EnableLogging); Assert.IsNull(options.LogAction); - Assert.IsNotNull(options.Buttons); - Assert.AreEqual(0, options.Buttons.Count); + Assert.IsNull(options.OnActivated); + Assert.IsNull(options.OnTimeout); + Assert.IsNull(options.OnError); + Assert.IsNull(options.DeadlineTime); + Assert.IsNull(options.DeadlineAction); } [Test] - public void Properties_CanBeSet() + public void NotificationOptions_SetProperties_ValuesAreCorrect() { // Arrange var options = new NotificationOptions(); - Action logAction = (log) => { }; + var button = new NotificationButton("Test", "test"); + var deferralOptions = new DeferralOptions(); + var deadlineAction = DeadlineAction.ExecuteCommand("test"); + var deadlineTime = DateTime.Now.AddHours(1); + Action logAction = (s) => { }; + Action resultAction = (r) => { }; // Act options.Title = "Test Title"; @@ -47,13 +51,23 @@ namespace WindowsNotifications.Tests options.HeroImagePath = "hero.png"; options.Attribution = "Test Attribution"; options.TimeoutInSeconds = 30; + options.Buttons.Add(button); options.Async = true; - options.Id = "custom-id"; + options.Id = "test-id"; options.Tag = "test-tag"; options.Group = "test-group"; - options.PersistState = true; - options.EnableLogging = false; + options.DeferralOptions = deferralOptions; + options.ShowReminder = true; + options.ReminderTimeInMinutes = 15; + options.PersistState = false; + options.EnableLogging = true; options.LogAction = logAction; + options.OnActivated = resultAction; + options.OnTimeout = resultAction; + options.OnError = resultAction; + options.DeadlineTime = deadlineTime; + options.DeadlineAction = deadlineAction; + options.ShowCountdown = true; // Assert Assert.AreEqual("Test Title", options.Title); @@ -62,91 +76,24 @@ namespace WindowsNotifications.Tests Assert.AreEqual("hero.png", options.HeroImagePath); Assert.AreEqual("Test Attribution", options.Attribution); Assert.AreEqual(30, options.TimeoutInSeconds); + Assert.AreEqual(1, options.Buttons.Count); + Assert.AreEqual(button, options.Buttons[0]); Assert.IsTrue(options.Async); - Assert.AreEqual("custom-id", options.Id); + Assert.AreEqual("test-id", options.Id); Assert.AreEqual("test-tag", options.Tag); Assert.AreEqual("test-group", options.Group); - Assert.IsTrue(options.PersistState); - Assert.IsFalse(options.EnableLogging); + Assert.AreEqual(deferralOptions, options.DeferralOptions); + Assert.IsTrue(options.ShowReminder); + Assert.AreEqual(15, options.ReminderTimeInMinutes); + Assert.IsFalse(options.PersistState); + Assert.IsTrue(options.EnableLogging); Assert.AreEqual(logAction, options.LogAction); - } - - [Test] - public void Buttons_CanBeAdded() - { - // Arrange - var options = new NotificationOptions(); - var button1 = new NotificationButton("Button 1"); - var button2 = new NotificationButton("Button 2", "custom-id", "custom-arg"); - - // Act - options.Buttons.Add(button1); - options.Buttons.Add(button2); - - // Assert - Assert.AreEqual(2, options.Buttons.Count); - Assert.AreEqual("Button 1", options.Buttons[0].Text); - Assert.AreEqual("Button 2", options.Buttons[1].Text); - Assert.AreEqual("custom-id", options.Buttons[1].Id); - Assert.AreEqual("custom-arg", options.Buttons[1].Argument); - } - } - - [TestFixture] - public class NotificationButtonTests - { - [Test] - public void Constructor_SetsText() - { - // Arrange & Act - var button = new NotificationButton("Button Text"); - - // Assert - Assert.AreEqual("Button Text", button.Text); - Assert.IsNotNull(button.Id); - Assert.IsTrue(Guid.TryParse(button.Id, out _)); - Assert.AreEqual(button.Id, button.Argument); - } - - [Test] - public void Constructor_SetsCustomId() - { - // Arrange & Act - var button = new NotificationButton("Button Text", "custom-id"); - - // Assert - Assert.AreEqual("Button Text", button.Text); - Assert.AreEqual("custom-id", button.Id); - Assert.AreEqual("custom-id", button.Argument); - } - - [Test] - public void Constructor_SetsCustomArgument() - { - // Arrange & Act - var button = new NotificationButton("Button Text", "custom-id", "custom-arg"); - - // Assert - Assert.AreEqual("Button Text", button.Text); - Assert.AreEqual("custom-id", button.Id); - Assert.AreEqual("custom-arg", button.Argument); - } - - [Test] - public void Properties_CanBeSet() - { - // Arrange - var button = new NotificationButton("Initial Text"); - - // Act - button.Text = "Updated Text"; - button.Id = "updated-id"; - button.Argument = "updated-arg"; - - // Assert - Assert.AreEqual("Updated Text", button.Text); - Assert.AreEqual("updated-id", button.Id); - Assert.AreEqual("updated-arg", button.Argument); + Assert.AreEqual(resultAction, options.OnActivated); + Assert.AreEqual(resultAction, options.OnTimeout); + Assert.AreEqual(resultAction, options.OnError); + Assert.AreEqual(deadlineTime, options.DeadlineTime); + Assert.AreEqual(deadlineAction, options.DeadlineAction); + Assert.IsTrue(options.ShowCountdown); } } } diff --git a/WindowsNotifications.Tests/NotificationResultTests.cs b/WindowsNotifications.Tests/NotificationResultTests.cs index 5c1c7e5..f0d673f 100644 --- a/WindowsNotifications.Tests/NotificationResultTests.cs +++ b/WindowsNotifications.Tests/NotificationResultTests.cs @@ -8,82 +8,84 @@ namespace WindowsNotifications.Tests public class NotificationResultTests { [Test] - public void Constructor_SetsNotificationId() + public void NotificationResult_DefaultValues_AreCorrect() { - // Arrange - string notificationId = "test-id"; - - // Act - var result = new NotificationResult(notificationId); - - // Assert - Assert.AreEqual(notificationId, result.NotificationId); - } - - [Test] - public void Constructor_SetsDefaultValues() - { - // Arrange - string notificationId = "test-id"; - - // Act - var result = new NotificationResult(notificationId); + // Arrange & Act + var result = new NotificationResult(); // Assert + Assert.IsNull(result.NotificationId); Assert.IsFalse(result.Displayed); Assert.IsFalse(result.Activated); Assert.IsFalse(result.Dismissed); Assert.IsNull(result.ClickedButtonId); Assert.IsNull(result.ClickedButtonText); Assert.IsNull(result.ClickedButtonArgument); - Assert.IsNull(result.ErrorMessage); + Assert.IsTrue((DateTime.Now - result.CreatedTime).TotalSeconds < 1); Assert.IsNull(result.InteractionTime); - // CreatedTime should be close to now - Assert.Less((DateTime.Now - result.CreatedTime).TotalSeconds, 5); + Assert.IsNull(result.ErrorMessage); + Assert.IsNull(result.ErrorCode); + Assert.IsFalse(result.Deferred); + Assert.IsNull(result.DeferredUntil); + Assert.IsNull(result.DeferralReason); + Assert.IsNull(result.DismissalReason); + Assert.IsNull(result.SystemAction); + Assert.IsFalse(result.DeadlineReached); + Assert.IsNull(result.DeadlineReachedTime); + Assert.IsNull(result.DeadlineAction); } [Test] - public void Error_CreatesErrorResult() + public void NotificationResult_SetProperties_ValuesAreCorrect() { // Arrange - string notificationId = "test-id"; - string errorMessage = "Test error message"; - - // Act - var result = NotificationResult.Error(notificationId, errorMessage); - - // Assert - Assert.AreEqual(notificationId, result.NotificationId); - Assert.AreEqual(errorMessage, result.ErrorMessage); - Assert.IsFalse(result.Displayed); - } - - [Test] - public void Properties_CanBeSet() - { - // Arrange - var result = new NotificationResult("test-id"); - DateTime interactionTime = DateTime.Now; + var result = new NotificationResult(); + var createdTime = DateTime.Now.AddMinutes(-5); + var interactionTime = DateTime.Now.AddMinutes(-2); + var deferredUntil = DateTime.Now.AddHours(1); + var deadlineReachedTime = DateTime.Now.AddMinutes(-1); // Act + result.NotificationId = "test-id"; result.Displayed = true; result.Activated = true; result.Dismissed = true; - result.ClickedButtonId = "button-id"; - result.ClickedButtonText = "Button Text"; - result.ClickedButtonArgument = "button-arg"; - result.ErrorMessage = "Error message"; + result.ClickedButtonId = "button1"; + result.ClickedButtonText = "OK"; + result.ClickedButtonArgument = "ok"; + result.CreatedTime = createdTime; result.InteractionTime = interactionTime; + result.ErrorMessage = "Test error"; + result.ErrorCode = "E001"; + result.Deferred = true; + result.DeferredUntil = deferredUntil; + result.DeferralReason = "User requested"; + result.DismissalReason = "User dismissed"; + result.SystemAction = "snooze"; + result.DeadlineReached = true; + result.DeadlineReachedTime = deadlineReachedTime; + result.DeadlineAction = "restart"; // Assert + Assert.AreEqual("test-id", result.NotificationId); Assert.IsTrue(result.Displayed); Assert.IsTrue(result.Activated); Assert.IsTrue(result.Dismissed); - Assert.AreEqual("button-id", result.ClickedButtonId); - Assert.AreEqual("Button Text", result.ClickedButtonText); - Assert.AreEqual("button-arg", result.ClickedButtonArgument); - Assert.AreEqual("Error message", result.ErrorMessage); + Assert.AreEqual("button1", result.ClickedButtonId); + Assert.AreEqual("OK", result.ClickedButtonText); + Assert.AreEqual("ok", result.ClickedButtonArgument); + Assert.AreEqual(createdTime, result.CreatedTime); Assert.AreEqual(interactionTime, result.InteractionTime); + Assert.AreEqual("Test error", result.ErrorMessage); + Assert.AreEqual("E001", result.ErrorCode); + Assert.IsTrue(result.Deferred); + Assert.AreEqual(deferredUntil, result.DeferredUntil); + Assert.AreEqual("User requested", result.DeferralReason); + Assert.AreEqual("User dismissed", result.DismissalReason); + Assert.AreEqual("snooze", result.SystemAction); + Assert.IsTrue(result.DeadlineReached); + Assert.AreEqual(deadlineReachedTime, result.DeadlineReachedTime); + Assert.AreEqual("restart", result.DeadlineAction); } } } diff --git a/WindowsNotifications.Tests/Properties/AssemblyInfo.cs b/WindowsNotifications.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..872b186 --- /dev/null +++ b/WindowsNotifications.Tests/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.Tests")] +[assembly: AssemblyDescription("Tests for the Windows Notifications library")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("WindowsNotifications.Tests")] +[assembly: AssemblyCopyright("Copyright © 2023")] +[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("b1c2d3e4-f5g6-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.Tests/UnitTest1.cs b/WindowsNotifications.Tests/UnitTest1.cs new file mode 100644 index 0000000..f0333c3 --- /dev/null +++ b/WindowsNotifications.Tests/UnitTest1.cs @@ -0,0 +1,18 @@ +using NUnit.Framework; + +namespace WindowsNotifications.Tests +{ + public class Tests + { + [SetUp] + public void Setup() + { + } + + [Test] + public void Test1() + { + Assert.Pass(); + } + } +} \ No newline at end of file diff --git a/WindowsNotifications.Tests/WindowsNotifications.Tests.csproj b/WindowsNotifications.Tests/WindowsNotifications.Tests.csproj index 58e37cd..d696e86 100644 --- a/WindowsNotifications.Tests/WindowsNotifications.Tests.csproj +++ b/WindowsNotifications.Tests/WindowsNotifications.Tests.csproj @@ -1,18 +1,73 @@ - - + + + + + - net472 - false + Debug + AnyCPU + {B1C2D3E4-F5G6-47A8-B9C0-D1E2F3A4B5C6} + Library + Properties + WindowsNotifications.Tests + WindowsNotifications.Tests + v4.7.2 + 512 + true + + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 - - - - + + ..\packages\NUnit.3.12.0\lib\net45\nunit.framework.dll + + + + + + + + + - - + + + + - + + + {A1B2C3D4-E5F6-47A8-B9C0-D1E2F3A4B5C6} + WindowsNotifications + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + diff --git a/WindowsNotifications.Tests/packages.config b/WindowsNotifications.Tests/packages.config new file mode 100644 index 0000000..217d892 --- /dev/null +++ b/WindowsNotifications.Tests/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/WindowsNotifications.sln b/WindowsNotifications.sln index d54fb3d..fb5ff82 100644 --- a/WindowsNotifications.sln +++ b/WindowsNotifications.sln @@ -1,24 +1,28 @@ + 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WindowsNotifications.Tests", "WindowsNotifications.Tests\WindowsNotifications.Tests.csproj", "{FAD1BEAF-FEEE-43B0-9DFA-BFC55779AE88}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + 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} + {FAD1BEAF-FEEE-43B0-9DFA-BFC55779AE88}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FAD1BEAF-FEEE-43B0-9DFA-BFC55779AE88}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FAD1BEAF-FEEE-43B0-9DFA-BFC55779AE88}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FAD1BEAF-FEEE-43B0-9DFA-BFC55779AE88}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/WindowsNotifications/Cmdlets/ClearAllNotificationResultsCmdlet.cs b/WindowsNotifications/Cmdlets/ClearAllNotificationResultsCmdlet.cs new file mode 100644 index 0000000..4721c5c --- /dev/null +++ b/WindowsNotifications/Cmdlets/ClearAllNotificationResultsCmdlet.cs @@ -0,0 +1,42 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Clears all notification results. + /// Clears all notification results from the database. + /// + [Cmdlet(VerbsCommon.Clear, "AllNotificationResults")] + [OutputType(typeof(bool))] + public class ClearAllNotificationResultsCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var result = _notificationManager.DeleteAllNotificationResults(); + WriteObject(result); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "ClearAllNotificationResultsError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/GetAllNotificationResultsCmdlet.cs b/WindowsNotifications/Cmdlets/GetAllNotificationResultsCmdlet.cs new file mode 100644 index 0000000..7694893 --- /dev/null +++ b/WindowsNotifications/Cmdlets/GetAllNotificationResultsCmdlet.cs @@ -0,0 +1,42 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Gets all notification results. + /// Gets all notification results from the database. + /// + [Cmdlet(VerbsCommon.Get, "AllNotificationResults")] + [OutputType(typeof(WindowsNotifications.Models.NotificationResult))] + public class GetAllNotificationResultsCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var results = _notificationManager.GetAllNotificationResults(); + WriteObject(results, true); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetAllNotificationResultsError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/GetInteractiveUserSessionsCmdlet.cs b/WindowsNotifications/Cmdlets/GetInteractiveUserSessionsCmdlet.cs new file mode 100644 index 0000000..17333af --- /dev/null +++ b/WindowsNotifications/Cmdlets/GetInteractiveUserSessionsCmdlet.cs @@ -0,0 +1,42 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Gets interactive user sessions. + /// Gets all interactive user sessions. + /// + [Cmdlet(VerbsCommon.Get, "InteractiveUserSessions")] + [OutputType(typeof(string))] + public class GetInteractiveUserSessionsCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var sessions = _notificationManager.GetInteractiveUserSessions(); + WriteObject(sessions, true); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetInteractiveUserSessionsError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/GetNotificationResultCmdlet.cs b/WindowsNotifications/Cmdlets/GetNotificationResultCmdlet.cs new file mode 100644 index 0000000..87affef --- /dev/null +++ b/WindowsNotifications/Cmdlets/GetNotificationResultCmdlet.cs @@ -0,0 +1,48 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Gets a notification result. + /// Gets the result of a notification by its ID. + /// + [Cmdlet(VerbsCommon.Get, "NotificationResult")] + [OutputType(typeof(WindowsNotifications.Models.NotificationResult))] + public class GetNotificationResultCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The ID of the notification. + /// + [Parameter(Mandatory = true, Position = 0)] + public string NotificationId { get; set; } + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var result = _notificationManager.GetNotificationResult(NotificationId); + WriteObject(result); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "GetNotificationResultError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/InitializeWindowsNotificationsCmdlet.cs b/WindowsNotifications/Cmdlets/InitializeWindowsNotificationsCmdlet.cs new file mode 100644 index 0000000..90bcf0f --- /dev/null +++ b/WindowsNotifications/Cmdlets/InitializeWindowsNotificationsCmdlet.cs @@ -0,0 +1,54 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Initializes the Windows Notifications system. + /// Initializes the Windows Notifications system with the specified database path. + /// + [Cmdlet(VerbsData.Initialize, "WindowsNotifications")] + [OutputType(typeof(bool))] + public class InitializeWindowsNotificationsCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void ProcessRecord() + { + try + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + + WriteVerbose("Windows Notifications initialized successfully."); + WriteVerbose(string.Format("Database path: {0}", _notificationManager.GetDatabaseFilePath())); + + // Check if running as SYSTEM + bool isSystem = _notificationManager.IsRunningAsSystem(); + WriteVerbose(string.Format("Running as SYSTEM: {0}", isSystem)); + + // Check for interactive user sessions + var sessions = _notificationManager.GetInteractiveUserSessions(); + WriteVerbose(string.Format("Interactive user sessions: {0}", sessions.Count)); + foreach (var session in sessions) + { + WriteVerbose(string.Format(" - {0}", session)); + } + + WriteObject(true); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "InitializeWindowsNotificationsError", ErrorCategory.NotSpecified, null)); + WriteObject(false); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/RemoveNotificationResultCmdlet.cs b/WindowsNotifications/Cmdlets/RemoveNotificationResultCmdlet.cs new file mode 100644 index 0000000..458ff8f --- /dev/null +++ b/WindowsNotifications/Cmdlets/RemoveNotificationResultCmdlet.cs @@ -0,0 +1,48 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Removes a notification result. + /// Removes a notification result from the database. + /// + [Cmdlet(VerbsCommon.Remove, "NotificationResult")] + [OutputType(typeof(bool))] + public class RemoveNotificationResultCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The ID of the notification. + /// + [Parameter(Mandatory = true, Position = 0)] + public string NotificationId { get; set; } + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var result = _notificationManager.DeleteNotificationResult(NotificationId); + WriteObject(result); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "RemoveNotificationResultError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/ShowNotificationCmdlet.cs b/WindowsNotifications/Cmdlets/ShowNotificationCmdlet.cs new file mode 100644 index 0000000..75873c2 --- /dev/null +++ b/WindowsNotifications/Cmdlets/ShowNotificationCmdlet.cs @@ -0,0 +1,123 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Shows a notification. + /// Shows a notification with the specified options. + /// + [Cmdlet(VerbsCommon.Show, "Notification")] + [OutputType(typeof(WindowsNotifications.Models.NotificationResult))] + public class ShowNotificationCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The title of the notification. + /// + [Parameter(Mandatory = true, Position = 0)] + public string Title { get; set; } + + /// + /// The message body of the notification. + /// + [Parameter(Mandatory = true, Position = 1)] + public string Message { get; set; } + + /// + /// The buttons to display on the notification. + /// + [Parameter(Mandatory = false)] + public string[] Buttons { get; set; } + + /// + /// Whether to run the notification asynchronously. + /// + [Parameter(Mandatory = false)] + public SwitchParameter Async { get; set; } + + /// + /// The timeout in seconds (0 = no timeout). + /// + [Parameter(Mandatory = false)] + public int TimeoutInSeconds { get; set; } + + /// + /// The path to the logo image. + /// + [Parameter(Mandatory = false)] + public string LogoImagePath { get; set; } + + /// + /// The path to the hero image. + /// + [Parameter(Mandatory = false)] + public string HeroImagePath { get; set; } + + /// + /// The attribution text. + /// + [Parameter(Mandatory = false)] + public string Attribution { get; set; } + + /// + /// The deadline time for the notification. + /// + [Parameter(Mandatory = false)] + public DateTime? DeadlineTime { get; set; } + + /// + /// Whether to show a countdown timer on the notification. + /// + [Parameter(Mandatory = false)] + public SwitchParameter ShowCountdown { get; set; } + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var options = new WindowsNotifications.Models.NotificationOptions + { + Title = Title, + Message = Message, + Async = Async.IsPresent, + TimeoutInSeconds = TimeoutInSeconds, + LogoImagePath = LogoImagePath, + HeroImagePath = HeroImagePath, + Attribution = Attribution, + DeadlineTime = DeadlineTime, + ShowCountdown = ShowCountdown.IsPresent + }; + + if (Buttons != null && Buttons.Length > 0) + { + for (int i = 0; i < Buttons.Length; i++) + { + options.Buttons.Add(new WindowsNotifications.Models.NotificationButton(Buttons[i], string.Format("button{0}", i), Buttons[i])); + } + } + + var result = _notificationManager.ShowNotification(options); + WriteObject(result); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "ShowNotificationError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/TestSystemContextCmdlet.cs b/WindowsNotifications/Cmdlets/TestSystemContextCmdlet.cs new file mode 100644 index 0000000..7479289 --- /dev/null +++ b/WindowsNotifications/Cmdlets/TestSystemContextCmdlet.cs @@ -0,0 +1,42 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Tests if running as SYSTEM. + /// Tests if the current process is running as SYSTEM. + /// + [Cmdlet(VerbsDiagnostic.Test, "SystemContext")] + [OutputType(typeof(bool))] + public class TestSystemContextCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + } + + protected override void ProcessRecord() + { + try + { + var result = _notificationManager.IsRunningAsSystem(); + WriteObject(result); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "TestSystemContextError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Cmdlets/WaitNotificationCmdlet.cs b/WindowsNotifications/Cmdlets/WaitNotificationCmdlet.cs new file mode 100644 index 0000000..b09c140 --- /dev/null +++ b/WindowsNotifications/Cmdlets/WaitNotificationCmdlet.cs @@ -0,0 +1,60 @@ +using System; +using System.Management.Automation; + +namespace WindowsNotifications.Cmdlets +{ + /// + /// Waits for a notification to complete. + /// Waits for a notification to complete and returns the result. + /// + [Cmdlet(VerbsLifecycle.Wait, "Notification")] + [OutputType(typeof(WindowsNotifications.Models.NotificationResult))] + public class WaitNotificationCmdlet : PSCmdlet + { + private WindowsNotifications.NotificationManager _notificationManager; + + /// + /// The ID of the notification. + /// + [Parameter(Mandatory = true, Position = 0)] + public string NotificationId { get; set; } + + /// + /// The timeout in seconds (-1 = wait indefinitely). + /// + [Parameter(Mandatory = false)] + public int TimeoutInSeconds { get; set; } + + /// + /// The path to the database file. + /// + [Parameter(Mandatory = false)] + public string DatabasePath { get; set; } + + protected override void BeginProcessing() + { + _notificationManager = string.IsNullOrEmpty(DatabasePath) + ? new NotificationManager() + : new NotificationManager(DatabasePath); + + // Set default timeout to -1 (wait indefinitely) + if (TimeoutInSeconds == 0) + { + TimeoutInSeconds = -1; + } + } + + protected override void ProcessRecord() + { + try + { + var result = _notificationManager.WaitForNotification(NotificationId, TimeoutInSeconds * 1000); + WriteObject(result); + } + catch (Exception ex) + { + WriteError(new ErrorRecord(ex, "WaitNotificationError", ErrorCategory.NotSpecified, null)); + } + } + } +} diff --git a/WindowsNotifications/Models/DeadlineAction.cs b/WindowsNotifications/Models/DeadlineAction.cs index a63c224..549d820 100644 --- a/WindowsNotifications/Models/DeadlineAction.cs +++ b/WindowsNotifications/Models/DeadlineAction.cs @@ -1,79 +1,51 @@ using System; -using System.Diagnostics; namespace WindowsNotifications.Models { /// - /// Represents an action to take when a notification deadline is reached + /// Represents an action to take when a notification deadline is reached. /// public class DeadlineAction { /// - /// The type of action to take + /// Gets or sets the type of deadline action. /// - public DeadlineActionType ActionType { get; set; } = DeadlineActionType.None; + public DeadlineActionType ActionType { get; set; } /// - /// The command to execute (for Process action type) + /// Gets or sets the command to execute when the deadline is reached. /// 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) + /// Gets or sets the script to execute when the deadline is reached. /// public string Script { get; set; } /// - /// The custom action to execute (for Custom action type) + /// Gets or sets the action to execute when the deadline is reached. /// - public Action CustomAction { get; set; } + public Action Action { get; set; } /// - /// Creates a new DeadlineAction with no action + /// Creates a new deadline action that executes a command. /// - 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) + /// The command to execute. + /// A new deadline action. + public static DeadlineAction ExecuteCommand(string command) { return new DeadlineAction { - ActionType = DeadlineActionType.Url, - Url = url + ActionType = DeadlineActionType.Command, + Command = command }; } /// - /// Creates a new DeadlineAction that executes a PowerShell script + /// Creates a new deadline action that executes a PowerShell script. /// - /// The PowerShell script to execute + /// The PowerShell script to execute. + /// A new deadline action. public static DeadlineAction ExecuteScript(string script) { return new DeadlineAction @@ -84,89 +56,43 @@ namespace WindowsNotifications.Models } /// - /// Creates a new DeadlineAction that executes a custom action + /// Creates a new deadline action that executes a .NET action. /// - /// The custom action to execute - public static DeadlineAction ExecuteCustomAction(Action action) + /// The action to execute. + /// A new deadline action. + public static DeadlineAction ExecuteAction(Action action) { return new DeadlineAction { - ActionType = DeadlineActionType.Custom, - CustomAction = action + ActionType = DeadlineActionType.Action, + Action = 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 + /// Defines the types of deadline actions. /// public enum DeadlineActionType { /// - /// No action + /// No action. /// None, /// - /// Run a process + /// Execute a command. /// - Process, + Command, /// - /// Open a URL - /// - Url, - - /// - /// Execute a PowerShell script + /// Execute a PowerShell script. /// Script, /// - /// Execute a custom action + /// Execute a .NET action. /// - Custom + Action } } diff --git a/WindowsNotifications/Models/DeferralOptions.cs b/WindowsNotifications/Models/DeferralOptions.cs index d169bac..c561aeb 100644 --- a/WindowsNotifications/Models/DeferralOptions.cs +++ b/WindowsNotifications/Models/DeferralOptions.cs @@ -4,112 +4,63 @@ using System.Collections.Generic; namespace WindowsNotifications.Models { /// - /// Options for configuring notification deferrals + /// Options for configuring notification deferrals. /// public class DeferralOptions { /// - /// Whether deferrals are enabled for this notification + /// Gets or sets whether deferrals are enabled. /// public bool Enabled { get; set; } = true; /// - /// The maximum number of times the notification can be deferred + /// Gets or sets the maximum number of deferrals allowed. /// public int MaxDeferrals { get; set; } = 3; /// - /// The available deferral options to show to the user + /// Gets or sets the default deferral time in minutes. /// - public List DeferralChoices { get; set; } = new List(); + public int DefaultDeferralTimeInMinutes { get; set; } = 60; /// - /// The text to display for the deferral button + /// Gets or sets the list of predefined deferral options in minutes. + /// + public List DeferralOptions { get; set; } = new List { 15, 30, 60, 240, 480 }; + + /// + /// Gets or sets the text for the deferral button. /// public string DeferButtonText { get; set; } = "Defer"; /// - /// The text to display for the deferral selection prompt + /// Gets or sets the text for the deferral dropdown. /// - public string DeferralPrompt { get; set; } = "Defer until:"; + public string DeferDropdownText { get; set; } = "Defer for:"; /// - /// Whether to schedule a reminder when the notification is deferred + /// Gets or sets the format for the deferral option text. /// - public bool ScheduleReminder { get; set; } = true; + public string DeferralOptionFormat { get; set; } = "{0} minutes"; /// - /// Whether to enforce the maximum number of deferrals + /// Gets or sets the format for the deferral option text when the time is in hours. /// - public bool EnforceMaxDeferrals { get; set; } = true; + public string DeferralOptionHourFormat { get; set; } = "{0} hours"; /// - /// The action to take when the maximum number of deferrals is reached + /// Gets or sets the absolute deadline after which no more deferrals are allowed. /// - public DeadlineAction MaxDeferralsAction { get; set; } + public DateTime? AbsoluteDeadline { get; set; } /// - /// The current number of times this notification has been deferred + /// Gets or sets the text to display when the absolute deadline is approaching. /// - public int CurrentDeferralCount { get; set; } = 0; + public string DeadlineApproachingText { get; set; } = "Final deadline approaching. No further deferrals will be allowed."; /// - /// Creates a new DeferralOptions instance with default values + /// Gets or sets the time in minutes before the absolute deadline when the deadline approaching message should be shown. /// - 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; - } + public int DeadlineApproachingWarningMinutes { get; set; } = 120; } } diff --git a/WindowsNotifications/Models/NotificationButton.cs b/WindowsNotifications/Models/NotificationButton.cs new file mode 100644 index 0000000..410b311 --- /dev/null +++ b/WindowsNotifications/Models/NotificationButton.cs @@ -0,0 +1,64 @@ +namespace WindowsNotifications.Models +{ + /// + /// Represents a button in a notification. + /// + public class NotificationButton + { + /// + /// Initializes a new instance of the class. + /// + public NotificationButton() + { + } + + /// + /// Initializes a new instance of the class with the specified text and ID. + /// + /// The text to display on the button. + /// The unique identifier for the button. + public NotificationButton(string text, string id) + { + Text = text; + Id = id; + } + + /// + /// Initializes a new instance of the class with the specified text, ID, and argument. + /// + /// The text to display on the button. + /// The unique identifier for the button. + /// The argument to return when the button is clicked. + public NotificationButton(string text, string id, string argument) + { + Text = text; + Id = id; + Argument = argument; + } + + /// + /// Gets or sets the text to display on the button. + /// + public string Text { get; set; } + + /// + /// Gets or sets the unique identifier for the button. + /// + public string Id { get; set; } + + /// + /// Gets or sets the argument to return when the button is clicked. + /// + public string Argument { get; set; } + + /// + /// Gets or sets the background color of the button (hex format: #RRGGBB). + /// + public string BackgroundColor { get; set; } + + /// + /// Gets or sets the foreground (text) color of the button (hex format: #RRGGBB). + /// + public string ForegroundColor { get; set; } + } +} diff --git a/WindowsNotifications/Models/NotificationOptions.cs b/WindowsNotifications/Models/NotificationOptions.cs index 70470d0..4c17fca 100644 --- a/WindowsNotifications/Models/NotificationOptions.cs +++ b/WindowsNotifications/Models/NotificationOptions.cs @@ -4,286 +4,123 @@ using System.Collections.Generic; namespace WindowsNotifications.Models { /// - /// Options for configuring a notification + /// Options for configuring a notification. /// public class NotificationOptions { /// - /// The title of the notification + /// Gets or sets the title of the notification. /// public string Title { get; set; } /// - /// The main message body of the notification + /// Gets or sets the main message body of the notification. /// public string Message { get; set; } /// - /// Optional logo image path or URL + /// Gets or sets the optional logo image path. /// public string LogoImagePath { get; set; } /// - /// Optional hero image path or URL + /// Gets or sets the optional hero image path. /// 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 + /// Gets or sets the optional attribution text. /// public string Attribution { get; set; } /// - /// Optional timeout in seconds. If set to 0, the notification will not timeout. + /// Gets or sets the optional timeout in seconds (0 = no timeout). /// - public int TimeoutInSeconds { get; set; } = 0; + public int TimeoutInSeconds { get; set; } /// - /// Optional list of buttons to display on the notification + /// Gets or sets the optional list of buttons to display. /// public List Buttons { get; set; } = new List(); /// - /// Whether to run the notification asynchronously + /// Gets or sets whether to run the notification asynchronously. /// - public bool Async { get; set; } = false; + public bool Async { get; set; } /// - /// 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 + /// Gets or sets the optional unique identifier for the notification. /// public string Id { get; set; } = Guid.NewGuid().ToString(); /// - /// Optional tag for grouping notifications + /// Gets or sets the optional tag for grouping notifications. /// public string Tag { get; set; } /// - /// Optional group name for grouping notifications + /// Gets or sets the optional group name for grouping notifications. /// public string Group { get; set; } /// - /// Optional deferral options for notifications that can be deferred + /// Gets or sets the optional deferral options. /// public DeferralOptions DeferralOptions { get; set; } /// - /// Whether to show a reminder if the notification is not interacted with + /// Gets or sets whether to show a reminder if the notification is not interacted with. /// - public bool ShowReminder { get; set; } = false; + public bool ShowReminder { get; set; } /// - /// Time in minutes after which to show a reminder if ShowReminder is true + /// Gets or sets the time in minutes after which to show a reminder. /// public int ReminderTimeInMinutes { get; set; } = 60; /// - /// Whether to persist the notification state in the database + /// Gets or sets whether to persist the notification state in the database. /// - public bool PersistState { get; set; } = false; + public bool PersistState { get; set; } = true; /// - /// Whether to enable logging for this notification + /// Gets or sets whether to enable logging for this notification. /// - public bool EnableLogging { get; set; } = true; + public bool EnableLogging { get; set; } /// - /// Optional action to handle logging (if null, logs to Debug output) + /// Gets or sets the optional action to handle logging. /// public Action LogAction { get; set; } /// - /// Optional action to execute when the notification is activated + /// Gets or sets the optional action to execute when the notification is activated. /// public Action OnActivated { get; set; } /// - /// Optional action to execute when the notification times out + /// Gets or sets the optional action to execute when the notification times out. /// public Action OnTimeout { get; set; } /// - /// Optional action to execute when an error occurs + /// Gets or sets the optional action to execute when an error occurs. /// public Action OnError { get; set; } /// - /// Optional serialized data for reminders + /// Gets or sets the optional deadline time for the notification. /// - 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; } + public DateTime? DeadlineTime { get; set; } /// - /// Optional identifier for the button + /// Gets or sets the action to take when the deadline is reached. /// - public string Id { get; set; } = Guid.NewGuid().ToString(); + public DeadlineAction DeadlineAction { get; set; } /// - /// Optional argument to pass when the button is clicked + /// Gets or sets whether to show a countdown timer on the notification. /// - 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; - } + public bool ShowCountdown { get; set; } } } diff --git a/WindowsNotifications/Models/NotificationResult.cs b/WindowsNotifications/Models/NotificationResult.cs index 32cca07..9e17bbf 100644 --- a/WindowsNotifications/Models/NotificationResult.cs +++ b/WindowsNotifications/Models/NotificationResult.cs @@ -3,127 +3,103 @@ using System; namespace WindowsNotifications.Models { /// - /// Represents the result of a notification interaction + /// Represents the result of a notification interaction. /// public class NotificationResult { /// - /// The unique identifier of the notification + /// Gets or sets the unique identifier of the notification. /// public string NotificationId { get; set; } /// - /// Whether the notification was successfully displayed + /// Gets or sets whether the notification was successfully displayed. /// public bool Displayed { get; set; } /// - /// Whether the notification was activated (clicked) + /// Gets or sets whether the notification was activated (clicked). /// public bool Activated { get; set; } /// - /// Whether the notification was dismissed + /// Gets or sets whether the notification was dismissed. /// public bool Dismissed { get; set; } /// - /// The ID of the button that was clicked, if any + /// Gets or sets 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 + /// Gets or sets 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 + /// Gets or sets the argument of the button that was clicked, if any. /// public string ClickedButtonArgument { get; set; } /// - /// The time when the notification was created + /// Gets or sets the time when the notification was created. /// public DateTime CreatedTime { get; set; } = DateTime.Now; /// - /// The time when the notification was interacted with, if any + /// Gets or sets the time when the notification was interacted with, if any. /// public DateTime? InteractionTime { get; set; } /// - /// Any error message that occurred during the notification process + /// Gets or sets any error message that occurred during the notification process. /// public string ErrorMessage { get; set; } /// - /// The error code, if an error occurred + /// Gets or sets the error code, if an error occurred. /// public string ErrorCode { get; set; } /// - /// Whether the notification was deferred + /// Gets or sets whether the notification was deferred. /// public bool Deferred { get; set; } /// - /// The time when the notification was deferred until, if applicable + /// Gets or sets the time when the notification was deferred until, if applicable. /// public DateTime? DeferredUntil { get; set; } /// - /// The reason for deferral, if applicable + /// Gets or sets the reason for deferral, if applicable. /// public string DeferralReason { get; set; } /// - /// The reason for dismissal, if applicable + /// Gets or sets the reason for dismissal, if applicable. /// public string DismissalReason { get; set; } /// - /// The system action that was taken (e.g., snooze, dismiss) + /// Gets or sets the system action that was taken (e.g., snooze, dismiss). /// public string SystemAction { get; set; } /// - /// Whether the deadline was reached + /// Gets or sets whether the deadline was reached. /// public bool DeadlineReached { get; set; } /// - /// The time when the deadline was reached, if applicable + /// Gets or sets 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 + /// Gets or sets 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/Models/SimpleNotificationOptions.cs b/WindowsNotifications/Models/SimpleNotificationOptions.cs deleted file mode 100644 index 46e6dee..0000000 --- a/WindowsNotifications/Models/SimpleNotificationOptions.cs +++ /dev/null @@ -1,116 +0,0 @@ -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 - /// - public string LogoImagePath { get; set; } - - /// - /// Optional hero image path - /// - public string HeroImagePath { 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 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; } - - /// - /// 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; } - } - - /// - /// 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; } - - /// - /// 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; - } - } -} diff --git a/WindowsNotifications/Models/SimpleNotificationResult.cs b/WindowsNotifications/Models/SimpleNotificationResult.cs deleted file mode 100644 index feea485..0000000 --- a/WindowsNotifications/Models/SimpleNotificationResult.cs +++ /dev/null @@ -1,84 +0,0 @@ -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; } - - /// - /// 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 index a947c00..4067650 100644 --- a/WindowsNotifications/NotificationManager.cs +++ b/WindowsNotifications/NotificationManager.cs @@ -1,114 +1,118 @@ using System; using System.Collections.Generic; +using System.IO; 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 + /// The main entry point for the Windows Notifications library. /// public class NotificationManager { - private readonly UserSessionManager _sessionManager; - private readonly ToastNotificationService _toastService; + private readonly string _databasePath; private readonly DatabaseService _databaseService; - private readonly Dictionary _reminderTimers = new Dictionary(); - private readonly Dictionary _deadlineTimers = new Dictionary(); - private readonly Timer _cleanupTimer; + private readonly ToastNotificationService _toastService; + private readonly UserSessionManager _sessionManager; /// - /// Gets or sets the path to the LiteDB database file + /// Initializes a new instance of the class with the default database path. /// - public string DatabasePath { get; private set; } - - /// - /// Creates a new NotificationManager with the default database path - /// - public NotificationManager() : this(null) + public NotificationManager() : this(GetDefaultDatabasePath()) { } /// - /// Creates a new NotificationManager with the specified database path + /// Initializes a new instance of the class with the specified database path. /// - /// The path to the LiteDB database file, or null to use the default + /// The path to the database file. public NotificationManager(string databasePath) { - DatabasePath = databasePath; - _sessionManager = new UserSessionManager(); + _databasePath = databasePath; + _databaseService = new DatabaseService(_databasePath); _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)); + _sessionManager = new UserSessionManager(); } /// - /// Shows a notification with the specified options + /// Shows a notification with the specified options. /// - /// The notification options - /// The result of the notification + /// The notification options. + /// The notification result. 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()) + // Generate a unique ID if not provided + if (string.IsNullOrEmpty(options.Id)) { - try + options.Id = Guid.NewGuid().ToString(); + } + + // Log the notification + LogNotification(options, "Showing notification"); + + // Check if there's an interactive user session + if (!_sessionManager.HasInteractiveUserSession()) + { + var result = new NotificationResult { - result = impersonation.ExecuteAsUser(session.SessionId, () => _toastService.ShowNotification(options)); - } - catch (Exception ex) + NotificationId = options.Id, + Displayed = false, + ErrorMessage = "No interactive user session found", + CreatedTime = DateTime.Now + }; + + LogNotification(options, "No interactive user session found, notification not displayed"); + + if (options.PersistState) { - result = NotificationResult.Error(options.Id, $"Failed to show notification: {ex.Message}"); + _databaseService.SaveNotificationResult(result); } + + return result; } - // Set up a reminder if requested - if (options.ShowReminder && result.Displayed && !result.Activated && !result.Dismissed) + // Show the notification + NotificationResult notificationResult; + if (IsRunningAsSystem()) { - SetupReminderTimer(options); + // Show notification using impersonation + notificationResult = _sessionManager.RunAsInteractiveUser(() => _toastService.ShowNotification(options)); } - - // Set up a deadline timer if specified - if (options.DeadlineTime.HasValue && result.Displayed && !result.Activated && !result.Dismissed) + else { - SetupDeadlineTimer(options, result); + // Show notification directly + notificationResult = _toastService.ShowNotification(options); } - // Persist the result if requested - if (options.PersistState && result != null) + // Save the result if persistence is enabled + if (options.PersistState) { - _databaseService.SaveNotificationResult(result); + _databaseService.SaveNotificationResult(notificationResult); } - return result; + // If not async, wait for the notification to complete + if (!options.Async) + { + notificationResult = WaitForNotification(options.Id); + } + + return notificationResult; } /// - /// Shows a simple notification with the specified title and message + /// 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 + /// The title of the notification. + /// The message body of the notification. + /// The notification result. public NotificationResult ShowSimpleNotification(string title, string message) { var options = new NotificationOptions @@ -121,267 +125,173 @@ namespace WindowsNotifications } /// - /// Shows a notification with buttons + /// 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 + /// The title of the notification. + /// The message body of the notification. + /// The buttons to display. + /// The notification result. 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() + Message = message }; + if (buttons != null && buttons.Length > 0) + { + for (int i = 0; i < buttons.Length; i++) + { + options.Buttons.Add(new NotificationButton(buttons[i], $"button{i}", buttons[i])); + } + } + return ShowNotification(options); } /// - /// Shows a reboot notification with deferral 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 + /// The title of the notification. + /// The message body of the notification. + /// The text for the reboot button. + /// The text for the defer button. + /// The notification result. 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 + DeferralOptions = new DeferralOptions + { + Enabled = true, + DeferButtonText = deferButtonText + } }; + options.Buttons.Add(new NotificationButton(rebootButtonText, "reboot", "reboot")); + return ShowNotification(options); } /// - /// Gets the result of a notification + /// Gets the result of a notification. /// - /// The ID of the notification - /// The notification result, or null if not found + /// The unique identifier 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); + return _databaseService.GetNotificationResult(notificationId); + } - // If not found, try to get from the database - if (result == null) - result = _databaseService.GetNotificationResult(notificationId); + /// + /// Waits for a notification to complete. + /// + /// The unique identifier of the notification. + /// The timeout in milliseconds, or -1 to wait indefinitely. + /// The notification result. + public NotificationResult WaitForNotification(string notificationId, int timeout = -1) + { + DateTime startTime = DateTime.Now; + NotificationResult result = null; + + while (true) + { + result = GetNotificationResult(notificationId); + + if (result != null && (result.Activated || result.Dismissed || result.DeadlineReached || !string.IsNullOrEmpty(result.ErrorMessage))) + { + break; + } + + if (timeout > 0 && (DateTime.Now - startTime).TotalMilliseconds > timeout) + { + break; + } + + Thread.Sleep(500); + } return result; } /// - /// Waits for a notification to complete + /// Gets all notification results from the database. /// - /// 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 + /// A list of notification results. public List GetAllNotificationResults() { return _databaseService.GetAllNotificationResults(); } /// - /// Deletes a notification result from the database + /// Deletes a notification result from the database. /// - /// The ID of the notification - /// True if the deletion was successful, false otherwise + /// The unique identifier of the notification. + /// True if the notification was deleted, false otherwise. public bool DeleteNotificationResult(string notificationId) { return _databaseService.DeleteNotificationResult(notificationId); } /// - /// Deletes all notification results from the database + /// Deletes all notification results from the database. /// - /// True if the deletion was successful, false otherwise + /// True if all notifications were deleted, false otherwise. public bool DeleteAllNotificationResults() { return _databaseService.DeleteAllNotificationResults(); } /// - /// Gets the path to the database file + /// Gets the path to the database file. /// - /// The database file path + /// The path to the database file. public string GetDatabaseFilePath() { - return _databaseService.GetDatabaseFilePath(); + return _databasePath; } /// - /// Sets up a reminder timer for a notification + /// Checks if the current process is running as SYSTEM. /// - /// 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 + /// True if running as SYSTEM, false otherwise. public bool IsRunningAsSystem() { - return UserImpersonation.IsRunningAsSystem; + return _sessionManager.IsRunningAsSystem(); } /// - /// Gets all interactive user sessions + /// Gets all interactive user sessions. /// - /// A list of interactive user sessions + /// A list of interactive user sessions. public List GetInteractiveUserSessions() { - return _sessionManager.GetInteractiveUserSessions() - .Select(s => s.ToString()) - .ToList(); + return _sessionManager.GetInteractiveUserSessions(); + } + + private static string GetDefaultDatabasePath() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + string dbDirectory = Path.Combine(appDataPath, "WindowsNotifications"); + + if (!Directory.Exists(dbDirectory)) + { + Directory.CreateDirectory(dbDirectory); + } + + return Path.Combine(dbDirectory, "notifications.db"); + } + + private void LogNotification(NotificationOptions options, string message) + { + if (options.EnableLogging) + { + string logMessage = $"[{DateTime.Now}] [{options.Id}] {message}"; + options.LogAction?.Invoke(logMessage); + } } } } diff --git a/WindowsNotifications/Properties/AssemblyInfo.cs b/WindowsNotifications/Properties/AssemblyInfo.cs index ce253d2..86ad166 100644 --- a/WindowsNotifications/Properties/AssemblyInfo.cs +++ b/WindowsNotifications/Properties/AssemblyInfo.cs @@ -6,11 +6,11 @@ using System.Runtime.InteropServices; // 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: AssemblyDescription("A library for displaying Windows notifications from SYSTEM context")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] [assembly: AssemblyProduct("WindowsNotifications")] -[assembly: AssemblyCopyright("Copyright © 2024")] +[assembly: AssemblyCopyright("Copyright © 2023")] [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] diff --git a/WindowsNotifications/Resources/LiteDB.dll b/WindowsNotifications/Resources/LiteDB.dll index 213eeec9118d2c3478e32f350880e398a4b15d0d..14798bb869a28bd5afd8fb9f57ca18563a2b695f 100644 GIT binary patch literal 123 zcmXAhK?=h#3{z2C z8}ZcS{kBvytZH2F@M(66LATww(Nj{xuQ+1h8^bD`tjK_Ru~GRSNy#yzx4QJ>d|iHl C$18pS literal 355328 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 z zSQLISoVLCt;#J1~YsB9+_?N`%41SNe&)_kOfvxYqNyOXX5vKPzVk>`E#_)y2R~i0~ ziBn6W@T=i0U2pIc#8&=3OT5we4?7&V1CKEN?TM}Ozgx_IzZgD*_`wNL_@%^_{-+aL z^3EZ)^tmL4uO+tn&s&M_!6Ve~MdH^D9&$vrywSwgc=G__!|({hUroHm;FpOv8r*&) z@I*X9{=STjT3{iLLtb0P#b3gz{b{-e_>+QNWhGLJaRlydNH6_!Y$G z7<^w0KTQ0D@!v$8J~|3N2Tt1_`w%ZQ{x~V93Rj3NdtDI2mlIp|y@A+D|82zg;t}e< zp4dw78^qRl_8-LmHvC;nfi3w{i7ozF#Fl>ZV*CdYFTo>>zZ~N~n|QVH|4z*R$1(gA zvGu+1Ld<_7@%wm$@eMo%cnBUL?jg436L%)I^xrGyza-{=Y7AFm_!8o4@d)F4j@YUX zFB4n&|6$C(=2-aG;}M3RN<7QpV~NWKUrTJse;|gRB(~yvk=RP_Tf|m;AIALM!mqpmEB+2*E57l9@d(p%67fX_KScbz!Hp*X zx8f1uqy}?fqKWp%Z#5E^I@_OJb?P%~x#1(^oK>V1&uModwaONc7 zW<0ch#4`dzbEKQjvdF0qxLpAx6bk^eSumL?kP6I=4nCbr~X9P__AhS$XK-7)+i zvDN?mnAnp4iy^%ycU$QKc@HUFBJZK4EBVekT?0zj!qiwa$*;~ zc)ANz$i)h(aI$Q?FUh6n-*w#5-3X~xSf#~{m>9Dh>^`N%`A@s#HqJ)|Ad(>`lvd{_sCkBkA3!^}zb;#Q&)t z)BDjQsYat>xAYI%_9kdk z#PYrR6tL+#P0X|=hLO15NR8y2@@bYV+I7`!1}ngu?!zp4(G(_znHsuOQ) zA`E5MV!3f5@hbB!ms45P(UlzAg5x)tNgZ8V^OlEK&_4LAg%VL|4+wR=9;M0BNE7Z0 zk$qjIS0aXblP3dDkMhL(Eo3*Oy$>OxF*9Q1w;ESsDN zII~YkGrx~)nD&U^4T{H81q5J8--!b!N;{4?BJ2uU{7S3sVfh#S0 ze?A$~U%X6y{nZnO~3iir1;bAS*pjEJ|8NHx->Ya-&6B)%rZTOuNLn#T3b z=~98&wl$30wQ|~zg)%v!95;>eq)YQ6%2Cs95XycLW%0C&gmOegIjL|4%D~33EMVWi zlnI=CA#a>aA#a@iA#a?jA@6NqGP$b-1xQ^1iha-Y5s>qLij7OkofkJM91e{zLEj;e zl2Ml3hTIc_^2%NPxQl|F3C>n#xv1YTZUy!}J1mE}!a#okykx0)Urnyh?Q0tArhN8D zE%h4`*p0)ry)`)6G1HZ3;I^A1ol+S|Rh!-dmfUbzF3vG1-N#J06@g{L(pt4)>91J0@yeiB? zDojmEHdEE|l?+_GxXd`x?dhs8kEt*j6{b;zsgH+wt18T|RhT9frd5S$j)%#dVdufK zD$EcSW@{B@SUk)-Rr&Uk3NuQD8LPsKiHB*fis!E?OqU8XQH2>F4>PhV%sVQ~G!Qo+`}Tc$gidFzBaJiL=olyv}wU zyX$cOJW9N7ba7Iu)k&#d0R6~b2(P-`#`Y`@YFEA54ws^wyQOmEsW$GhCupw2VUrsf zt27x$0eu*ugMdDb&@MokGd1TFpymjT0opo3IiT?oO19&Q_4fKgU3+68IRwYn;=Y7r6u}cyCI9{{x@()KF(Tq!C z*pKBVcH&_ArgYkS65Mi31a9qFMvhE=bsHOq9B%0vjuvArZc`8WdiSO!y7FUt_kyD@ zm4%tgvmv`<6@7S_MnQI`*3*8tc8~1I;2^xJD%O5XZq!f=SH?b^%x-YB3UegGT!(#+ zW0k1m6&X7UlO46e$$Sk+2B(lqS6K|T|E!1F;sD`QO%MkNuW5ofKzLmf!~w!zG(jAD zJ8Ena5INqvC?D>Y5Br3$^0THEa?ooeJip8U}W9JYhyq2)ELe zV}|qcl#eK!Zuba63R|@8fo-Li)oiTm~psT=SsZ+DR@)tj-D>-qfljo zw<$vQP!5A07~WwIC3&!e`_pcm%FvTd@Py<~5FJiPX8*|(l$~aDqWH%%Uw@7F8&Npy zd-@xKH>7d#Z(fE=x$OF{Q*^7uT~|RIIF|G8VDe``JKjr(s8hWgL6OSHRQ09)g&4~_ zOJ$^ww+fZf-vu~$2heH%RKyj!oC>i&2R`ZwFJ?mU7J2f*g59=VnmuI3cHC^&&sC~$ zDkKG9J1&y>F_VeWF4LKW(vLo>(s^>Iy$a9GT@op{7Slx(3nS4OwbecO_U0W%Wrf8Z4`zqkp_!|v~tWlz*#)Z<88 z@IG+dp5%$y6Jvz_?di(tNgG9HhC@<6z$>)p1xOF<$$=ztg#Ox|AHp5lv#x@yL-*4T zL-=#}fPJ&UN8q3x7zziiREYhd@WIVf_;gI*CSKm}iO&W+&;zI=0h?;QxoEY6_ZT`a zfwe&?C=??G{f6T)SuiL?D}!PXM1yL3KK7WgrwLAbiZ1zz5v_q?`FA_QU|pw&%Y4m{}cj{z-*wLmz5D)LrdVaQX!P~>$# ziW>YIp^*KQj(dT^Xe?Qmi>zmP;rESl5IX}{StwAocdQd{M`=)b$W+GiP!b|}h(W33 zA=4W0fjs<_Zl+Q45Cc*>4|hddVCUg(z&a0|1j<yW;~hJddNUau`DRi}9$d zEWy(ep1R5zBy%FgsT_bnxVson8Sk;P7y^&rAkxv8P;_CcQz7;-&XdMu1>UMLDWDx` zstpb%cV3uv#$!HQ>KIZ_3uRM$FbCUTnP#$)SCjzX2a`pFlTr6mAsgY`~0y8xw2UfY~KwU;!K$ zD=froRRuoLSfNuLVaAEo;jX_UkCQV)ghgPti^?IC!oJ=~pdk;(2xN+tl|w=5x2)i7 zTzt#CD`{ppWY?9@CM+wfAQNTf6N+vPBy!vq2sVFfCT1~Qv@|`Y-A^W59Z27;=(~iz zbro717YLGEk184*E@a#wh_`ns8IK@E5rQM>&Wpr+cKvyj8AgCB2H{tnR#&+Ufr=B< z+nL#Qna*^F5@<;n4wPheysR5?2e zE>+HEAyF)6{MUj!zw7%twwEkA=G9{8K$|AXM6FWxpKfcBCQ1CrXI-f1o}3lbp+ydDkB+tkHkI-&cTVk z)j9Q{4Zz&LGss55ztU2m9=TS3?>fAF7_B zn(%A{wrBVxVtYouI+-3v-**$f5W`aga={+-sT?Oodk)-AWj8!|otIM~_K#C0^heUj zdy)foqr3vHA|m!jzy}{a6^fFc4yR4ijhl>v*?i}PA2vT!hYdrrIqLd><1wvhi$uDJ zL6?BB4#F-eZtcjy-bzxNmNYm_lqwyV3t{Q7+pRR)tXR86tyRoyw?uc{Zc$IGqzIID z>u_Y?hf!Xuc8dly_LQ- z{GmGO1mh`q%gk7Di0dzdF}!opw>Xu(LHCE?;~Bjthj!|x(j%S9KJ;)aHXSgw8bIyvxs9&qA+0+1v3Vm zZSgu%TI|LsnZ&*n=DyUqV!=@?-f0y}TXiflDrWSS^#vlSdQAM!C_j|_nPV~gONL|% z>0J4U(EbO3DPV@c4)f_|UWk2rBj@*Ix_ zpkoz%yz{`LK0_`7WXsCP3(Lxw)lD_jsi*@MC%DNXUnT)dSzSdvqp~*GRh6fhsq0If zz=-|;WmLV%1gj7PwUgrYhl`|BNtH(Y%Z|s4LnF?ZRI7X?2r{6w+vN`A6aVb-q!Gq; zSya{SGNZO?rogCP{wuPDs+TmevADF$B3kMtrdp+4z5q-CZGd`t9^JGJN}c0jVpZNU z!)(>dFN2}-B|LB#I-dS-q(QY9E;S67^viIWVOVV#Oj*4gM7ykB2^^NyJyE>S{GvT3 z6UyqQg>6%KdTra}skjd7lL-wVbq|B9yJ15ICSX#jHn@f*HnicGKUyHM0k<#TNf)l^ zW{2G5yi{<}e zyioonB^anPl_Ob%Zer3@Tq>74OG5g z!yx>1fBG}b55*xi@t@$(@@58W|3t;I8Z6}-A+0f^7z0`mZgNS)yy+g%P^we8o}Oaa zyuSodcrB<{xN<%z%~EkT9Bed;(-l*uFLe`S+Z99t2Uk#9UR1#M-2KQJtx*^=E6R+c z5-(Xm=NfyiWzKMu$Hy5)dbQg6^GT@~lP))1sL9mV`OL&KQXVHwDOY{+DnFTelNmAwp{L5vhr;~47nFeMmGb#Wb$)(>>_$ZJKftEm&43M6 zy(3>(??y}Elz>j0vfNrPhLE!$ARrYY5G2}Dff3FZhT82 zPV$B29&^B|9U(1O#vEI!K8NEUe*oBf1Rhq=FCn(@lYl;^=r_tjkRtg){!81@>luGx#24A^)g={eZ;CirjPlHtJY!guXH2068_AjqD1ag9s^(RQFvH6zTD(EeI&t0;BSP&kEEbhF0Z))-%SG@*tw6O?nmTa9sB@Fa^wWDGzUgYK_@`4!T_) zehIAG+1kXHpt|Zi$QRb3Q>6|`KpgH3x>`(~l`h2RG!gF@F3qmPvg^vP;orMdn6O^$ zP&oQhSeTV6{|pn69joZWpYjV{j*rJdl#3OEU%&(AVk}aB=#W;M9#;Mcl>FHpx*~Gq z4|!GoFg3CK5xpgU-VXEUH(&~=C-Ul7bTdtoSMOEl)svvxdG!=rS32s=7;L>gUJe1n46gKt%1boRMx?>_ZU2^3UfL$QB45R$23vFGk`4K49vv zBG%=a(OGsA7?tak*o~o;-I%j6yNPJA+Xv8q<@#wb1)!smvvjI+fou?#dtVZ1JXWqHz-EHU3B{%tEHZ zVNQkEp9mjJYRH@>Cpe3{)Ty~i0aHUCPT4kw9&~5?H~GPw34TYJc@e2?C^{(NV#DjO z^a_cB4H==bVPD*ak33>*XavxPqNTPWqqA%%FsgfXQQjyUGRI>!6wzYCPeU6%52k?m z1$(|gH*-zwnS|1oJzoUfw&!|aWzPxBd#OX@fjvK2n4iKGfLn963$t7KZf8T2%xPt5 zUB}Cak!!YMxb}oIQl;I&?H{eoEBxa;Rz2Em^_wQ;hW35{p0M}3jI%WmrJTwJ`1c-* zhgCh#Kmx<+Ngvgp9aqhBCV5Vl`C0mSXl)C=&;IOBfGjJFpm3y9A@)y!52~=5zj{SD zK2aQFv3rFaBT$;~<7w~`J@ev+VzC=%l|)s=CB@=N$761wScoJQgZN0qr7XX#l8JS^6{Rc3FB2Soe8O0+YE3Ri4Nf_IX=NpCcvh{z1QOfW4 zSa}laP9AC7IP;sItX9JggB+gu zwsE7%5B6muN>ah%DzbEOWyF?^1x9^YLzIWg#U$mO#ZQ8d&yb@?me1mAywX zKel9V5mg~G?Jcsjy&17(Z-J4$@q+~F3_~k>(>gJGi)gX;NN7NNzXPU#`6>4HK(*}s zKIpc+KLn23n>?G@yNl`T-`+{iEhI3ifUXQ1vo{y0p?<%t>bEF>1;5}^_Wn0qv^PDb zU8U+bY0BRJqHl#yV{bti5~vH*gAd4(m+-K6Ma#myRVp0qJ>oNH$eNw?+wnkkFKhFU zqWMKeo0cKw7e!kFrGO|T=3gw;ow)(?ldQ~NVD`}b(b)B0|1$MmWL3yb`-?Dbf5vRt zUtnZ^{IneAsj@$<6|=vH7W;QX1={~3Fa^wCvHwI+E&G2Ax^4eYfaCTj&t~?Y!UXE{ zaXe$%cV+)3(UoC8*Z!ga7Au2K*+1bb`_n_)pEPCv&p>r5d>Z=;Lf4)0#?W?~$dV5j zUfZ8;+Fv;PxBqf&e_0flQ)PczD`tNYE%xt$3bcP8m;&an*nbA7mi=-1L6!Y+YiMYHvyPQKVt=#O zWk)8jzx7{E;u4g3^_|IZ7|U{D-rmR+F3gkYoJtZt%GxQoSVQT-4@hFMG)JGbl=?(> z$d(r_*i`nah$zKRLv&#_g<8Q1l%cG!J=KI2^!MYr=bAcb!qWy4ueJeWvuq$RvO%-h zfT5KQm}4;;h-k6Fc2Iyes0CBNRErIE2Gz1b8g#o3W`NBY(afonFB~JPIduu>RI;F# zf5aao>nJH7kS-%dI%K3s4=Y6iVJAKA#rhDmE96#bq6V=vNnDbqIboV`xI!{u+>)kw z)oE$~-A>a0V3VflY}VyFihN<3M#;EW1#H23RBr-nb0Y=i1Ck|4bV!ouVI@f*(=!3G zTO~bYRq0{uvGhnhlAgUJJskTUP@RT>2xX_Ci802;zU0}gZCn63TWA{{=^L@62>H#h zpW8Ny0#0Qx_*89ag^RU?9=dHLO|^|J^sVsew;}6f>?;UE?Jze7ktHwTb=yc;h1tFM zO(u!!ox|{~d1VJiXvjCprB4|d3X4TVMw^x)ZKEjK!aGT!fGA|Ojm!%9rNM~cfg?0!<@ia^#;5v=PdwXn=DFe=Yj{t@P{vOg^sv%iQI`yT{# zSVx<|6fm!)JRb_GRY!B6+hqZls`gu+$yZgLRlt)u**J;iIZr|PfMhApbVzxohm|0K zx;(?2NDo<6dKh~wJra+kXR)M*<@xC9Gz>u~I}O7aW2`)rXS4FW401lVJY%IN^kdl1 zEzhEWQ`ri9syw&D#qvxKU7kr(<#{-LD|~V(sVdKc@Y$16xjB?9c?qw}Gi9MXUyVLN z%Cm%!@;n(vs4mZ95s}fRWk`7zMOy-;fGA{@XXXaVGs&tvAIt3dkIJ*ituQ3Zvk2DZ znOa!p7Z{aital9aSJ|JIi`id9i~Wy*IxNpygDGHMOL;yCRI5CX0NpMNqk#KspUGEM zo>jmvvpi2>c^*kY`G908&vZz6riYavfx0{olk|{PrH8S{(j)OmdVEO_%kzrrG>k?l zI}II-F;I_n-wRKCyS#`OMl+&-T$>t}=W)~s<(VFG zkve7{NK@t6qc87P#hy-Go&|9ktIEwWWXVf-l;_j6tildqI2jSiXUhtVS9ADoIZcmS z#TpX6O^27VEK0WU&QwG$%1Bv0QodW56DZ3htFn9sv!-g?`oRZg%p|fz2+OYs)8&^j zTjmxRm0xUCK>1~8Wp7$4W^WNK_C6gdu>5WVCSE+N%5N{IR{8A&-L`!ZSlQl;Tgej} zx1Pu3^*3%!a=&E)ISV}*E}VnCN7?%sWp9bpsdO>!%6NHB#M7F1?;#NdfSiTXoeFW) z#5-3)7Z-=~>jL(x^JjA6UcfFG^~wZ2r>jCPK00 zi;Y(ye%iPjOab!~?g?}=SK*#SH}h87u8Y8J*?wElZQD-)Hf@)gQzc*6cBwg42`I~# zg2|MT4@i+_iw;QTjf^>#Mu|hxc(J6B?bhYh`cFkDTmR{d zQraz3cF7YfyVpQUzh&3{=3=zc*#J|ep$&E*TVA-d4MddU*EXP5umNQ#8{9-Sw_pPko;HwpwG9}X zWdnhc4KOc;vd_@U2F$UT4Meoq;6^Aw8_WVzz*LJ3ZUfb_!Oo!D^#H3mHnRbFVm7#o ziTffOFkH+A5*@ZX!AIF(cerQ+de8>Mkqyl61-p#4rKhW0X=8~;>ZS=atv*- zFWKaTfT%wZQHo#NfLg%@l%Z_!U8=bS8<_C4fyAqAz}PGs2#jpdAvR!WWdr6|%myM_ zZ15c@KpPwYCY}eNZ15vcEgKvJx^08`z;PRpCuW03n7A*p0mH>?AkkqfAbeCkSOgbs zKo8o0II;nz977u%Ot!pm>3Sfd6u-6swSo;OL)qYAs<{OlnDDfL#H($<*en|ejBGGg zY{1aU2F$UT4Meoq;3rUkHdqLzfT@;x@HnWJ4Y1?B$_9r4$8A8Km<@i##C?$s7%pZ5 ziB3)k2yJjUZTvM5%0WB-!%9rEMUh6u-6swSo;O zL)qYIs<{OlnDDfL#H($<*en|ejBMbE4H#P4fH@Ykfru6xtb+oy!BQ{ za4hJy4VD4NZ9txw4PIj6zQ_g)7qfvxhoe*Aqik>@T(ki_XanNN23#zrY;YXe^1`KU zAfgn%wgI(*4Jbp|;Ez;u3pOy}X#1p_L7oV=)_uXtBWyP=Gc# z0Zai?EjIWQsFn>*0^PQO4;;4vd15wror(J*8!%kV1`?f9IR!q-1|_&?1A5Q~#E}gK zL^e2?YsK@Do#550;ZHFI?INB1-XV8&E6QfHIT~-lm#cuz?9r8%Vs`28_+JfxyTH z1+f7`D;qG!Vm1)bVuQD!0BvwOm;$C+Z15hamJQAX-L^pmIBo;-#BA^p6Zb_nV7QnK zBs!;Z7JQTqdf}oC=s_D0M>gQET&gWvNw&OjX&ZCOmB* z@oF0|Hp>PABO4UO1`Mriz#NO&KtziTK7azWK>((JsTLb-0@bp?*`V7tI0rax1MSVcB$#K%k9Ktw5iZ3Air8&HO_ zLEq=tz=Wp_BwlR;#%9?-U}S?Xu>nIX8!*RWHW1NbgHJ;noCl_WsTLa~p{Heo3qZGR za1n6a2IPs^AoHKtfZ<{`km#Jsm*Asp@MXAY1A5Q~#E}i~b8={d3(1xjE^Px5rTDcC zs1|M7+kiYV8w_URzNjr?xR?zjI;V0Oe3T8YfQvSu2W>zc*?_<3sCsZI z+491rZ6Kl)zqSFjf(KF5P%Rr=3A$~AtAXP-AWzH&!q7=Wj0kwh+C_~v`2-Vzz4NQ32K;qRlU~HBR1V%Q%nhAWP zF|@J)b1Y^95iK@og95a{wO|UEYOz5(sFn?`2i>;84Zv|5{D17d2b5Js);4@@>_8Km zru&kEq-)=9a%hm$Aff~Ti3X4$n7P;uh+Gb~I+!tzIb*ar|zYZnfL$x_5I&kU$515YFF*rwQE1S_Nh}RT=L{$(2av@41=ii7Vd4Bd!B3u z4ESN<3sH!Boqxdr_d3-nUe-tbnuGB$nf3m`s^I+t)|mGX4$0-okOvEy;_eNsNuI>_ z4{#nVb}xoRBFYX@eeq!e9Md?Z;{5~M-o$f@#pVg@^U0SGn-8gUG7SXjBjF?m2}mi# zK)Mr04M?-kfW7>3aIY&6Kup!Mk14yeFcq1_w68E_YlA5f$iq~kg=rUPKuj+JlS}&v z(en2aix<8z?!QyQ8iVO!xjb%w zDQhfD*Fz#PWe3ERbbu)yNcAwilzjN?0uDZ=(#bUNG36kEDTNqJQylg7FbxC{Q}yg) z%I+*oMP@O@vmltKY;7U_ zSCKCvHXl>zWE%LGa*)83LJXz@IO^?T8VDe!>e&f0N$29#5Mat0 zgXz*-o?`+`Sz}>(6C@H-c0f!?2bkiybPv<($(InDkEwJr4SY;FNMK4K2Gijj_4Y6g z1Q1j8>|@IAEKEgaF&!XG+1g-A1oAMIXkj`G8W7VP!Q|3@!gLHuElh7lxsB$ zPadYbb96hxl|@IAEKEgaF~yU4h`nrWFeL(c zm`b!T9SaSJ>Fr>0X+L2)5v3NUccR?J^ls4kn35+C)2STYjxc4rMln^V;mkd#UsIop z*PjARSz|C=mdmp?z?3x>ruRW2F=YqDlyrb8-VyLHy^DMavH6%vC)2>kl!FAO6k;%) z%u#O-(?9?*RnI=A?9ReeWEN9Aj|NQH+F(iq@-UTXVY(+YAg1?%$)){-=?s)wm~KM3 zjp+lR4W_}nh~yEbd>65u_b%c-9N+fdRT+YBNSNoo*cQ+G@g-CK<3_J@IR2l;e|J2s zehmKc4l&on1!#jcu@u5rJ*ZdaFG?RlYCRA3Bntr?Icu&%<8X#WIgf<#uEvmcsI#65 z8{&%ALtnhI9D)df+IN;-QpTS<)G2cYO zh-O#jTd{e9@Pdl>K$`@$_61#G3tAKidfOL--_A6m-}6hVYI1ZuEV(*Y$u_?1v9@fy zJLD_*u`ekr`*Xi!8`*BI?6$t_PkF`UgMK3kqvfFXJ_x?vKW1eQ%+q^KuIzTcY<@n_ zmz|pK3C3#FL)RA^S>@ZhPoChZ*(Y~p?Dn2u^IbYQbSuvvYK1!-iwOtKA24ga7us+L9~JXAF^GKZZ~XD~5Ql1iTtC?bABqe%mud9b29OogZ7svrTMSNF!`7w!lN2 z8jXQ%8;LD%!SlvpYTtqXc3?(qnScXgixk2)O{iDqd8IERwPMR_EQFhpv*t_2hYX9h zjV&*u&WjX^B@c@ZV#}>;>pgd>*zz(Zdo2`7UNHqhY#_8dQnw>%-?C+N>EO0u?2O=71Y`n^r1KZWxVSG zK{>HSqJ!A72d0t+t`3seK`!R60yl7~I!r=La4w0v(z=b`*-E>!VL@j(!A6}0!Dc(1 zVJExs&f5B&;bot|L@z4(NOVs0QArSec58fO$8N8)c7A941x_E2)FRIza>9{%3c^us z<2}u1RBFtI?Y*8%eA1+7(0Cn9OZCz09sG$cE>O?+b1$cs-Bt@tz#>M%Y%2PT*E8u=HP=j=xQCFVKD6`u|S zw-ulMigG(X{SCB6h*LuF_O-z33z#hsxVALV-M-HTC( z81)YOEO(_8zCMHcHOq6?viPAnf3JZxTJM@2+_%6@mto`75%}s0j@cbteB>jZ-i%in z)A-1AvHP}oe>1!(e3^s1Em4CnkC+!jUnfUG3|R5%^CQyAuxiDNpKaj4;TjTWI#kWE z#ulid62@r`p>gcB@Rwf(`-Mg{ndv9eTz;Z&js*R(QmXn+L@g~R66wEtxl7i@#)pV( zo)1Z~eCS|kM<03MXs8Aohm{rvL* z)`0-RDMfrZ*_{Qa$ZSkNe8-xFtqq>Eejc6@Ej;U?0rC7hm|WTp`S+Qp1(DyvJnbl) zSApBY`6HCuIDY~1!dDFAryXpyZ|%3g%W9u;J@#ffjq%xvc={1U zAFF*Eul?qu{Lbl@Qdi&2>hJd)gswkjmjN0QYzeKt1lV5|*k3Hnci#;cDoQ=hvuyYY`Gv5?GHFsM=DYU$p6aIIew^(mx)`^q49$d3*ZOTB(+;OLt1{IpES_y z?1Nus()7fKQCN$3>^a0QXat8~Ti+oxA|pi(abq}16Tu-U**L`cv{`NpMdar9iE?6S zAd0>r#eCmj$Chu1lyC4mptV_>8{Z(3dA=dh@{RML3w`6?U~*{|T~fPy=G;upPUr< z32XkRe$oUz!A}Yr>F}uB4o?m`WX=Dy!xGbB5e~*No3a3o$)TqvhBKt|9CJ|Mn9?^x zA?b&+?irzoge3UxDMK1ra?cI;bq62=_oRH|o>vem#4CFb?Ren8fBPIC$fAErKi@yu zv*n*6<)7R`!$}V7QMNYzNe>DfO5gF7X!+;mP=@|l3?`S>K|anreF6DG<_VL0bpyC9 zUu}kR+gDqF&i7UFab~#ma(nnZz716#!}xFmks1iw;3vEhs~ z&aiFzBuFtUp5|r{Be-)Ya*Il!C8-Jx2<@u{b%U`d9I`r2HFu61f@-3OCKOo!CyP6 z!yLC8zXo@iphfm$*{20Pvc~Q)r{RC>uiZ@FU2rh|+MNaPS6YGoN_rcAmBtyiO;5?L z2HUY^LJfU?l_`w~-(QIh{FP#jzur!;{v&@4q|slcnD4Lb+wxbDS$`cXe`Raquk@lk zf0bzY>upen{<;>$%;>j?B{^knhYqp_0Ge3vSC_d!XF**CgnCe}#5cSazc+h^?MYDyN%DO_>KU2xfy=^4ju$?_ zevA*?PXovY{Oh!L-U+VL0tv)j+WEM%KMQw}*>&G;!kw)R?nEOGcZnA6o1g-5uLP4z zTOwb@JS{4$9|4DjbuX0LSmPtn9@fFVZt@6g-s|@FcAn&58{gZBI{RXy*gWUSw$|Rx zqW~KZVK^J`9|9Bb?~4QQmm=XzU+RI>eDNG=BUBjUbv*Oxe8Ha-A`7Jt^FRc@1)%+S>j1f9jiyl$7k4skw}LL zIpTAB)JI?%>#>dBjV*U9>9jy5#GCSYKX}+U*fR0 zH{V`?SD5+6wwQ1Hz#WajZ{Y5P%GLPCT-!+RDR-p=zSKfpGQ*JCa}B>huenC1nrUnh z&alkPwGph%Fg0^c4mRH_(Oi=cOh%)CDeH175*J-=k>%wzl#7qR;DC9B&3E~nba;vm zXh~BCbL}pr#6-VK$*d>Y+1l^YcXM`0HxIN|e{l0a`K+5~nXK9*Zp6o3ji-$# z+_*UT7LdKw4tBx0w$M!gZyyj={@rE*T7-<`6!gzem)v>{u+Wjd25I_ zIqJsO5ZsD4YY4W(8iMbY2>^Or7kmJ#f1M zAP_+Oq?nH%yR+~UnZ<7p;m6hnKUz2sKZzE8uR{akw=0-jnh5#bn5UtH-@D+j@Y@~b zHh$wl=i^77JpA70NOy!E+ii=Vdc?1Rv*XMpq;^o6%u?V-CgMl>zr$}R@!Nx95|ZNM zr*4frA3qKP_^}Uz-`_c^?co;)AbwKJ$B*4v_=(KoH(vO$wZV@T&cjclh2MM7fcQ-W zlS>mJzbErFl<@lq92S04P;TS57wCNa$diZP=NxI{_%RHd`NwwN8XiCI!Sl*ZW(ulP zd;o=gXJ#O^A)LihKuD(4zW6zC9JhsVv^@m}dQ*^3^Le*mm7Qbmn@y#}garC%N&_QW zXr3QVa!5dv-5E4L1u5 z6U*sfa%n5%XEINV3Cpj+VPQENH~Jb76Dz;QN?B`>`UmTc$E#e)FkX$DC3 zh!1|Sg!88?6_h45a`T{75 zGeCXE0dU%7^RSX=Vf8IkAXfW<$)!1v zpUXUrBCP%e4hyS!D7Ud%06HHl^5kLlJBPX>tk`Z_tkh#TvydHU79q8HDe6~oafsd7XFim}|s4uXhID^%%9Kd$53M3FKDdA(q{w%CSX0e(itk~LM zMQi3^CDFp_7pOq24gix&b0EK%c^XAng^F^pItb-9R)>Jj$BI08Sj8b_hgh-Qwpgi0 zd=Z2lXAVbdW0htpup$$&BHb8PxDn)Gbue2b)WF9|J(#9GR@4_*QJldl$^mQ_t3U#= zk`g{v?9akVWEQJEg%w*HtZ2^QR=sg2ch`fUJY9ttZ2A1m_YVbz*L-4Rx7w=GucF`UV;DmZ}cViiarR#L*piv3wwiOgb!t!ZGz z)&?tDGY>0?7FOj@fmp2ulS^|TzlM1lMOd{5hlN!G%5AKU1)Yx-dGfI8!lCX6E4JGf zEA<%89LJ6`YmwSmoxoCHMJ8fJx-qP9v(Cfn7`8~Lfsd7XFim}|s4uXhID=Iu4q&@j z1rmsrl<=`)e->6EvsmGlA+Ta=gB7irhm}MNtBz2CSp5l1F3o}b@yydG!m2wsEUZpM zxsBDyp!2aJPaam49BSiOaeK(D1KG~Qst)VG^UXR?J%%% zJs7knW>Ut-jJ;WyiOgcQmoQ^%gBh)whnYkRvz}0Zn4JnHmu5l!H0Eg>Vb%*A7G`Il z+{WxI(D|5=Cl9m!9O#ZPW4moJQ;*@y+3YxT4pJMl^H>VZ$VALYZ;M%VE@s}(1fR*a z2{rXGQ(s1ij~O)wW)y5N>&u~R2eY6(F_SVrX6((vOk@@_Y)fLEv9-aBR?WjqqJ>!> zC_v2q3?`RmLH=CkX&hlT7#x+o>WAXca6St8Ep2?>ySTDrL=PiX7Dp^dF9L@R=_PCt zujKD*-i>K9UAmD8-vlp;Cw936h5E@Emes$E`wkfEr4*969O;@1km%+pyWSe#1zOaf zjQi)^8(5RX?;NEp2uo5`g+i>mHej1-$BIuiwMudkk1uKz=?8J~aMNGWRiXJQ@8|M< z%KKtS2A0%FpCxCe!uPlRoSHvpf6JSk77ik_ILye7)!;x=u;^<@UI*1MQDHqUTg+sZa9ZzSZ?OG>KR7M`0&w_$)8T9F8B_Nxuh| zz~iW|;n1n5f4HKuuyy)%5Oy_w^r4_I`2v% zG8!dWMiUv>Y<56C0&t~izep6OaKgqKh$LTVW&8Fqr>;|$2BHSB6`!!;Q+s?K0Og6<$#d|FMDA_K7i7+a8S(l&@)c=3#8})T+}S~lT6Y9? zCEQz4oZ81{B2}T<^q%4jXISKJL;|a_3t83ci!#N1$n5R)WhAF3LYkLdgqzm=PLqi# z)-th3&oBAB;6B;$8XMChfr0cqmK2eVXLvTIU0f!Zn>RC0gUH_d=Gyyql-u^c6SQyd zu!EOsytzp}%uRS6w&+Pn^*hMjw*DauW>rVLz)#Zu&6a(oH(8C|l$@uxI@f%fBfS}~ z7v}1B7h2i+-NP=l-eW(3e&oyQM?(7DP3}NHk-mQWNk6h0{U|w4KXoqs7G?E2l=}JS zmD}(JI+|thGcRy1Wv@Xtl71qBHv*iKN^f) z**_fpW5MWs1O61Lrb}fWr<}`Ry0&$Fp-N&Mre>&nML6>WD%Ph@&>d(oVN@{TLTHr8 zDgv7nI8VYJbAYKQ@yS5q-jLWjEAc5x?4Af`p0-4jJ2?xunBpXUaIQGn4Cj=p60hWN zsIV$xi1ZcWR2R5w1%2>NCz@spD$)QdRUA*_Ra%s@%k_4U$7>69wxTq+KZsg z^#o25Ud$yAt|xxoyY(RROWA|@tMPqGP}Hj(e?Putb9XEj;_=qVwR1MN4kwRCOnFDY zCw~HS;pAE#Dnr09?DTcQ{mpe&*xAJi5BKxsPVZe!p1$@hl;2;mk^N(d`x*`<1!E)7 zs{sFtF`g;l!PVt@9B?|&STZkxE!hvVq^e2XJtfZ4`q3Eos&M8Nlvg*YI|1Rt6cu`| z+bLsDOE%ZEB)}Yu1SrhV(F=73NVkKS&BaAk z@uD;@f8N294D2@D3gryX#qL|w0zY|A%~PrHYSPKmP=jeu8gpM@bwagxdIFmxczVJx zd#s4&#npI0l{lNCt>t*Rwq4DKK*lmfSsYLEt|l16bBzM;mQkJue~wWKwlPXuXl+=w zH`J2Scoing9}o6dGuWg(*urpWp&4vzZ?K#94tFw~_DG)%C+WxbNIjEQK)In!^kY?Y z@l>I4_027D%}Se_Yu4lF{a7v3Vp73Z^*ZA5IPwuMe@?O-3m9h+j@>|%;K)Bddc(g* zP{x1yKL7ap>&D~R53hHHF#)eU#P>>1;|gLU^ZZd2R!#)oTBwLbUtFskhU2{Kd^Bqn zMvJ_)id51X@K_9**U6l>E)(QmW}dE${Hx4!mLdN-^PFDDzrj2wnbtR_K#Fw@_cqGy zc=aym{QVE|+;g(NZT|=Es?s(+oDnYy7zWXFHhj?>Lm0Pf0!oJTP;=p z)puqysu4%_N7V4*2;Lh;Svd27(hrq>j5N6)ra#{JPPYLj?x!qAu%MKhdP>Z&DBT*B zJS$OdS*)TgUV|@hI;F8voFXtw>OM!nNSlF4!_Lyug5<8CQsL5qdaNDerMQs5 zIXS-O0SM5Q=_X{h#_KWU`bWXpy@54)D0CaG{LMF425w#oCg9?l|Lyy2j+uW>W;nu*R-zkmbWQj0S z8awUak&pA?r_!kV4n~6jMq6WVNxv{>(38ozw=~q%b5e8+OjQ<}sd^ecg$w&y?AX9H z96ZT`UwU<%>v637xEu5PY*@@6XxlsnrDB8#Oy+r=iikW z??UK=wcBT4N^b;+y6(z+4pL{XNcwPuKk6ZlGe98!1**GOm-djinksp0`YcobC8hlt zdAyEHNoKj7fiJbmE{?xK1=PRm`lMTYb6*p-FmYtq1mO^0Lo&iB-V$W;XZgzCFm92 zSD0Rc5fvKYPa+FGx>@y$n@dV#I6K9>vy*9c6+K&P)&j8&okq_tE~ttZ7`}w}or|WS zfuln@6@;C8oI4H+>2%>=w(xo*jbR=@oOlHTTYOFP60G95xu*FhF!J*`OPz0xkxdhS z9V>1wOXBG|;+4KkNU8Wr6q{>|gcXv%gBUv`{{R~Eryb52eV`kr2VyIk5RX39W^S`!BFCJhK6o zDr{laZ%vD;5gJm5hOw}}31&^7f(t13XR-e#_EdE^LpqI55HOUNM(b~Y(cJIJlMoAn z(5_rU8Fj=g?|34dz;qEUW9@r?_L&JgTn8Aj(D6_&U6~O37)2tKzDXNOB;Sr|Hx1Ew@VU)p#5A-tjC3T|{@&*u-! zjH}EUJU(|^z7}QB0jDnKAlSD44ugwZ4EgbhTZ9ANk7Z{OmTZmR35NO$;kU`taN6h2 zjxA>#UQ}g2^g243DYgLeyza_8aD1PepU%aGbE+@&qejIMbX8KO-N@*5>cPj5nKT;Y-|1V>*{lB zbelS-zpUOaKJ5k_|IYq;;N>?^)g5#CZJpaME~~|5dR)|hd=7J)TxQf6OYBI=p>wcM z(4I2KYQ=7m^t0)(%b{7EOzb9%d9hojQS5$LbKBtQ5>y1*HaNJ{2>Eu*6CusgC&3M8 zdWO!_35B*Z@kJ?XVbBHDwlj4lyJ9o-4ED^)m)$cbq4DiR?qJVcB-?RKahS$ORx>_I z&KsXP*Pi)v8Xxt1g}MjM-VLp6{d%xRPrtZ>y>fm=74wyR@b?rB@b?J%r@u2co{WF_ zxlCS10aw@Qdwr?~?@^jJveS948k zN=-kDaxKDb5v}t@utEASV68ZPPbiyw71?ThywuY#sJ@cbng`uWToxC4(~Qb%npHIj z4615@Arjj8{7iGxe)Az#TnM=O7OD;KkBl-ro~ z1|49AyA@7ax{wc;4Zs2S<#^ltVIM1rz|VG5QU)7^!0Iy$H{oH01ZbCP){oAlwmP=Y;`lYZ}l1?`9#J?u^Q&;lku*oVAOR zpX%H_l)RZ?to{uJx)60ovJh*33P>gKN)@YnqO~p+T)az0d|8XDgz-M7$ET|VY(H|y zxJr;L(?4s5E(N~E5U8YD=33!HT4p=f3L2$E@Nz&y(dB@y6-*_2VWl)xB{napr{zH+ z{rO*)wXg9XqMGMFk}Ut(0_AwEKpb2q$m0vGcn{3vhcizb%74B9H_kU-R2{D?SnVvuy-B`yYlZjG;I>_DSGG3SrR@l#u1m?3w^#TBjn~*-Vbp1f_=2rF zoDI_OVQ=7I>&ossK-lKNE7Mhh{DDr#d;bs%HFwq5GlIj;O za>r1Qgw%lR4fR#>w2^}?kb}`Aye#EWm!(wKxY~EL7EhY?dxb@?6aVdfF%e0{PvUry=B9{{pw=FcVO2 zJIo}|!MTNZD)4?buqPkREqmeMom+UO);d5U@G2E0Wu_o4$ov2d{Bz56mYX7d*i^8puy&o;H!~ae>5n9l@bt z1`4zD8W$y6NA{|Zvu>7MH==nciAM&C^|8f{vn}lQ2AhrDzHFvFs9>&>CvUDdK^KkA z^@v_|K2qanJGktAIAE^xHPOYG=x3UV&dS0Jz8Z#Yc+7$n_xpLbox7dIT*no=bHKv6 zMb=dR@M_XYY=H6vV4C7aG`3t)?mpy9h!GRApWn!9at@kQ|0SHH(xzx@Ij(=&nTc4$ zp;$5Bf0lJIq7i%cT2!!w*&RYqXNMN}A~7!FKMY6g0mI+f+Tc&C=HV~V!oLs-aPHAg zE)(J09Hkacb5U;NG!L{t$HF>SAEiFYhdH(g2kRP>`%=80eJ}ns-y|-anGd$~{g@%{ zV&pJ6RB#^)4rD=QA<`;-DS;L4{wzzlY^+(sOsY=-UhhFBS0 zV&{acdQ{EXmzvWFg5HZv^EUa-#crBwCQ>a643WVaZB^DT1`Ap)4+~jCSd@BL(0(ox z;Ie>ujvx60n5R*MQ6d+kgHUc`bO>l4qnPsmY@d-XB;^4)>1|JuRfI$yKU7{B@ z+Ls`cJ`bi!VJ3-yH^*fxcPZ*plfz(V4wvX`mF6XSP;ctIrq)#uTet@!0iNtsc&hhp zg!$34>Ks{{jVx=6^yA*@?D&kWX@NXjs}I?_jc04x!DRybrkUq3kw2Vy4oSA|L|X@Y zYz59N*o=SpQDN|83PKnJe!m#`bU!UBRa}hX^tlKC9vjZC(T;VcSoj3bD3LL_3T8VM zdtu&v5`@@p=&_+5b=hv?K+{(NbKTZqg|7o{xf~hjpknC&455Q)qr<*AIIPNilGec&il)*>`o^1aUl-gGnHdC+0|ITN2g!kD^3lB%UleuB-ZgMOxaTh zGq#-!tMP~Qio=Ecg|=dN23yKABtmV<`#&N#AcyY;{q#F(#S$Z(7;n~Obuzv{m4gxJ zxkpKrFIkDwcB%TXCg4*`m;06emTy{tc1$LqJIiX&Ic&u^hRD7qELNcuLU z#&LaaJm+~4ye5(!31#@y=f$FWlk$1+=;S3R&EREAJfye!QsiZrx|gA%xog`Tg7wQ7 z*+%0d>4DG;o=x_gjcC}K@r3uY8))@oEBi)9ul$dv)i!zd<@4)@MW|~%gB;ro?5doBr5{6g z-YXbaZEjV#9M@=}vY35?nbSz`GliX=&b$*ey`g(d&-ouq-P0l11J8s$0$J63^5kkC zhqxKC289PMp9VuoYD{6ev&N8TM}7I0&wNxB72paKys6d&mK`xFB4n$4r~Qng-Eo2G z*weeWGkHz!{gXksVRSqFaF7kjf{3}N!$L6JK?Ve+0JE?3J6^?^U141?;Q7j8cwY~z zG&2c~|I)`eZD`u3&%;QQA(mP>K8ZnD63Fj)jb|f z-i?7^8HEqY7H8%ld8rEn8eLC9(_A%2;7tY$AQElGyKq66 zK=8N$%#rlHEDcWQ_kpxe=bKoKG4kGjaNR|&yz4Hkt&1DI?uzLe;7_s<+hLe@qGxZ9 zWg}z<`*p0x(}Jij%v_7Kdv!sNMDk;tJPR|osT9xK7N?icd$AzzfTQ~W+VHZ9ZBqw@ zGo;h)QBxMJD2o+laG$q0{U8d<;;HhM@#I6umPSis-SLo7jC<i6*=QhaL# zcFN7=)H{6c2tU)r^#-Nm0$v>)5xsc6$^#UAsY(uyc&F)0O5? zqbSB}-{+@XBa3?|(%s)fVb9h&hGejg;k6v@-@)%F!K`D5Ro*&A;cE6L)*Jd zG^ub4&PM-kxQdPby__p~*pnv@`_-6N+rj>I=y|;KWIGJ;ejGgPy}jBy!GbGV^_{s7 z)q1X@x}_#1#>bMc;R30+afPOZ8H_wyw|iS>X;DRIY-b&v=5u-4d^JzzJ%BbuiZXG1 zo*``>%ZuUqoUKb^^?0)^?%qSrgcyBzjUs9ycyWEs0TNy?8+cc7#0<>5KTJ&R-|Gkj z2vhd$W6JJ)OmVR;66aU*e8ve`I~hD_{X9G+T6i7>4Y(GiJzXZqZ(^SI61ENCux9q- zfZE0u+fSI;x$~MldD!B~r^e@X)Hwm;eq4L5Y=`$u3$c+h9tNrhoazoQD4@p7)2ERm zCag$R;lf=78C(<=C-zRB!Oap}1#+&D!v{lR#A+VCdNQ1(MgW7&4H(ZMXYZ4&Sra@* z6SOB5Y?H-;y;)d@%wln(uwZM01+A5bg+%LqXff<@tuKQ~3|v0PV1VyNH`5*6c~ z<2IbF^EfkPY!b#h2x@|7<0&umI?~>_+*d!E4|nJ)i@o{Q!&MeZ6P?Wd2CuVDk`1Yb zu^~s9XG7^I8~zQIwA&jvy6g=3H<_mbvf*3EWKVe`I&H0g8wK6N{H>j!oX_iYM&JCy z!^xNncr;(>t4P!Df_HN~*_Uh5O3q#WwQ1wT564sv&VsM8MWCwtI%prCsPjJd!Ogt} z@&O+P02DY|@Q;`3^zBk%BsqA$fL-INrPzI!f-$gc!DdtNHV*!`M#cCZ%eB&W-w(2X z53>J2wuZaZ5J#z6(iz?ig=)G~m7-QuXZEh<8TM6$;KMEgE>Ys5WHeK^8PkMC^}}H_ z_phu>xEz?AOd~W3&^d-KK{!qw@c&7#t@C@$4zv37)ChF>-wdHmzA#w@ex9{T^^Nj# z4%qi|7)>Os?(Zv|EFY&8jE~b(0>{x_f=nbIKhKNVZ-B|AQ^3dHVxInh{5#B35&8Qi zjNiTLFUO(b0~BU`ANvWs0J7rAhp4vw^<%Q<#dh-K#rCTqy3zA?T&|4ABC)<;JA}?p zaPZFC*JJ!xvptFf0zK>TOy8$C-~n2s$xGoVuq5+!r?$g|@p&lF*||;qW=z2P_mH?> zvH{Kq(mg{e`4UEj*Hw(LqSDx!EhyCk@Erfv_!ZFCd$^r7dbqvwiDiz{sW^QwRsiOs zZy%uwE=svLi*(NRk>qf3;$T5%`7c^X#!a2>cU%)dgpjrljAgBZ08;H;A_ zx3DNZgQo5btlY0r z08>+xq5(6SHLS2~BrMGZ>{;3VjV-8Qo-HWK*kY4s3+m}I!T4j$Q#Irx%u_Sj;^ACd z#8GbBq7Zc8FYBPw+0uo4utgCLo-NX{1*DN@fC$`@q*feVlH`It-TVYmRR z>+N#SOlT7Jr5#`yAa*XXB z^=!v+xlCZMrp!|>$bZ(ld-G9$rffoYmP#I#*qm$ zW&|25EYL(+K96^MG3Ur_aHBQzaAT#x?O6{u+SFwNtjd|^kdQB9p6Us=mveEeK)H=u zYtZ>?2lC{t9bSi&?W`Ss#5m5A{%nWSXB!;6wZl!sy7=AKaKPGu^;kQ!#R1RgA{F3# z+m?hm2bGaSb=c#D(R~Vg&2hf7!zu%T

3VC*Cra9ECLo4z)O>=K+qg0=!;lb_Y7L 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 - /// Service for managing notification state persistence using LiteDB + /// Service for managing notification data in 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"; + private readonly string _databasePath; ///

- /// Creates a new DatabaseService with the specified database path + /// Initializes a new instance of the class. /// - /// The path to the database file, or null to use the default - public DatabaseService(string dbPath = null) + /// The path to the database file. + public DatabaseService(string databasePath) { - // 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); + _databasePath = databasePath; + EnsureDatabaseDirectoryExists(); } /// - /// Gets the database path, using the default if none is specified + /// Saves a notification result to the database. /// - /// 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 + /// The notification result to save. + /// True if the result was saved successfully, false otherwise. public bool SaveNotificationResult(NotificationResult result) { try { - return _liteDb.UpsertNotificationResult(result); + using (var db = LiteDBEmbedded.GetDatabase(_databasePath)) + { + var collection = db.GetCollection("notifications"); + + // Create index on NotificationId + collection.EnsureIndex(x => x.NotificationId); + + // Check if the notification already exists + var existingResult = collection.FindOne(x => x.NotificationId == result.NotificationId); + + if (existingResult != null) + { + // Update existing notification + collection.Update(result); + } + else + { + // Insert new notification + collection.Insert(result); + } + + return true; + } } catch (Exception) { @@ -67,15 +64,19 @@ namespace WindowsNotifications.Services } /// - /// Gets a notification result from the database + /// Gets a notification result from the database. /// - /// The ID of the notification - /// The notification result, or null if not found + /// The unique identifier of the notification. + /// The notification result, or null if not found. public NotificationResult GetNotificationResult(string notificationId) { try { - return _liteDb.GetNotificationResult(notificationId); + using (var db = LiteDBEmbedded.GetDatabase(_databasePath)) + { + var collection = db.GetCollection("notifications"); + return collection.FindOne(x => x.NotificationId == notificationId); + } } catch (Exception) { @@ -84,14 +85,18 @@ namespace WindowsNotifications.Services } /// - /// Gets all notification results from the database + /// Gets all notification results from the database. /// - /// A list of notification results + /// A list of notification results. public List GetAllNotificationResults() { try { - return _liteDb.GetAllNotificationResults(); + using (var db = LiteDBEmbedded.GetDatabase(_databasePath)) + { + var collection = db.GetCollection("notifications"); + return collection.FindAll().ToList(); + } } catch (Exception) { @@ -100,15 +105,19 @@ namespace WindowsNotifications.Services } /// - /// Deletes a notification result from the database + /// Deletes a notification result from the database. /// - /// The ID of the notification - /// True if the deletion was successful, false otherwise + /// The unique identifier of the notification. + /// True if the notification was deleted, false otherwise. public bool DeleteNotificationResult(string notificationId) { try { - return _liteDb.DeleteNotificationResult(notificationId); + using (var db = LiteDBEmbedded.GetDatabase(_databasePath)) + { + var collection = db.GetCollection("notifications"); + return collection.Delete(x => x.NotificationId == notificationId) > 0; + } } catch (Exception) { @@ -117,14 +126,19 @@ namespace WindowsNotifications.Services } /// - /// Deletes all notification results from the database + /// Deletes all notification results from the database. /// - /// True if the deletion was successful, false otherwise + /// True if all notifications were deleted, false otherwise. public bool DeleteAllNotificationResults() { try { - return _liteDb.DeleteAllNotificationResults(); + using (var db = LiteDBEmbedded.GetDatabase(_databasePath)) + { + var collection = db.GetCollection("notifications"); + collection.DeleteAll(); + return true; + } } catch (Exception) { @@ -132,13 +146,13 @@ namespace WindowsNotifications.Services } } - /// - /// Gets the path to the database file - /// - /// The database file path - public string GetDatabaseFilePath() + private void EnsureDatabaseDirectoryExists() { - return _dbPath; + string directory = Path.GetDirectoryName(_databasePath); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } } } } diff --git a/WindowsNotifications/Services/ToastNotificationService.cs b/WindowsNotifications/Services/ToastNotificationService.cs index a5347fa..48ebbf7 100644 --- a/WindowsNotifications/Services/ToastNotificationService.cs +++ b/WindowsNotifications/Services/ToastNotificationService.cs @@ -1,370 +1,319 @@ using System; -using System.Collections.Generic; using System.IO; using System.Linq; -using System.Runtime.InteropServices; +using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Windows; -using Windows.Data.Xml.Dom; -using Windows.UI.Notifications; +using System.Windows.Forms; +using System.Xml; using WindowsNotifications.Models; namespace WindowsNotifications.Services { /// - /// Service for creating and displaying Windows Toast Notifications + /// Service for displaying 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 + /// Shows a notification with the specified options. /// - /// The notification options - /// The result of the notification + /// The notification options. + /// The notification result. public NotificationResult ShowNotification(NotificationOptions options) { - var result = new NotificationResult(options.Id); + var result = new NotificationResult + { + NotificationId = options.Id, + CreatedTime = DateTime.Now + }; try { - // Register COM server for notifications - RegisterComServer(); - // Create the toast XML - string toastXml = CreateToastXml(options); - XmlDocument xmlDoc = new XmlDocument(); - xmlDoc.LoadXml(toastXml); + string toastXml = GenerateToastXml(options); - // Create the toast notification - ToastNotification toast = new ToastNotification(xmlDoc); + // Create a temporary file for the toast XML + string tempFile = Path.Combine(Path.GetTempPath(), $"toast_{options.Id}.xml"); + File.WriteAllText(tempFile, toastXml); - // Set up event handlers - var resetEvent = new ManualResetEvent(false); - _notificationEvents[options.Id] = resetEvent; - _notificationResults[options.Id] = result; + // Create a temporary file for the result + string resultFile = Path.Combine(Path.GetTempPath(), $"result_{options.Id}.json"); - // 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); + // Create the process arguments + string arguments = $"-ToastFile \"{tempFile}\" -ResultFile \"{resultFile}\""; - // 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) + if (options.TimeoutInSeconds > 0) { - resetEvent.WaitOne(); - result = _notificationResults[options.Id]; + arguments += $" -Timeout {options.TimeoutInSeconds}"; } - return result; + if (options.DeadlineTime.HasValue) + { + arguments += $" -Deadline \"{options.DeadlineTime.Value:yyyy-MM-dd HH:mm:ss}\""; + } + + if (options.ShowCountdown) + { + arguments += " -ShowCountdown"; + } + + // Launch the toast process + System.Diagnostics.Process process = new System.Diagnostics.Process(); + process.StartInfo.FileName = "powershell.exe"; + process.StartInfo.Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"& {{ Import-Module Microsoft.PowerShell.Management; [Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null; [Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null; $xml = [Windows.Data.Xml.Dom.XmlDocument]::new(); $xml.LoadXml('{toastXml}'); $toast = [Windows.UI.Notifications.ToastNotification]::new($xml); $notifier = [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('WindowsNotifications'); $notifier.Show($toast); }}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.CreateNoWindow = true; + process.Start(); + + result.Displayed = true; + + // If not async, wait for the process to complete + if (!options.Async) + { + process.WaitForExit(); + + // Wait for the result file to be created + int maxWaitTime = options.TimeoutInSeconds > 0 ? options.TimeoutInSeconds * 1000 : 30000; + int waitTime = 0; + int sleepTime = 100; + + while (!File.Exists(resultFile) && waitTime < maxWaitTime) + { + Thread.Sleep(sleepTime); + waitTime += sleepTime; + } + + // Read the result file if it exists + if (File.Exists(resultFile)) + { + string resultJson = File.ReadAllText(resultFile); + ParseResultJson(resultJson, result); + + // Delete the result file + try + { + File.Delete(resultFile); + } + catch { } + } + } + + // Clean up the temporary file + try + { + File.Delete(tempFile); + } + catch { } } catch (Exception ex) { - return NotificationResult.Error(options.Id, ex.Message); + result.Displayed = false; + result.ErrorMessage = ex.Message; } + + return result; } - /// - /// Gets the result of a notification - /// - /// The ID of the notification - /// The notification result, or null if not found - public NotificationResult GetNotificationResult(string notificationId) + private string GenerateToastXml(NotificationOptions options) { - 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) + StringBuilder xml = new StringBuilder(); + xml.AppendLine(""); + xml.AppendLine(" "); + xml.AppendLine(" "); + + // Title + xml.AppendLine($" {EscapeXml(options.Title)}"); + + // Message + xml.AppendLine($" {EscapeXml(options.Message)}"); + + // Logo image 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); - } + xml.AppendLine($" "); } - - // Hero image (large banner image) + + // Hero 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); - } + xml.AppendLine($" "); } - - // 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 + + // Attribution if (!string.IsNullOrEmpty(options.Attribution)) { - XmlElement attributionElement = doc.CreateElement("text"); - attributionElement.SetAttribute("placement", "attribution"); - attributionElement.InnerText = options.Attribution; - bindingElement.AppendChild(attributionElement); + xml.AppendLine($" {EscapeXml(options.Attribution)}"); } - - // 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 + + xml.AppendLine(" "); + xml.AppendLine(" "); + + // Actions 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 + xml.AppendLine(" "); + + // 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); + xml.AppendLine($" "); } - } - - // Add audio element if specified - if (!string.IsNullOrEmpty(options.AudioSource) || options.SilentMode) - { - XmlElement audioElement = doc.CreateElement("audio"); - - if (options.SilentMode) + + // Deferral options + if (options.DeferralOptions != null && options.DeferralOptions.Enabled) { - audioElement.SetAttribute("silent", "true"); + xml.AppendLine($" "); + + if (options.DeferralOptions.DeferralOptions.Count > 0) + { + xml.AppendLine($" "); + + foreach (var deferOption in options.DeferralOptions.DeferralOptions) + { + string text = deferOption >= 60 + ? string.Format(options.DeferralOptions.DeferralOptionHourFormat, deferOption / 60) + : string.Format(options.DeferralOptions.DeferralOptionFormat, deferOption); + + xml.AppendLine($" "); + } + + xml.AppendLine(" "); + } } - else if (!string.IsNullOrEmpty(options.AudioSource)) - { - audioElement.SetAttribute("src", options.AudioSource); - - if (options.LoopAudio) - audioElement.SetAttribute("loop", "true"); - } - - toastElement.AppendChild(audioElement); + + xml.AppendLine(" "); + } + + xml.AppendLine(""); + + return xml.ToString(); + } + + private void ParseResultJson(string json, NotificationResult result) + { + try + { + // Simple JSON parsing without dependencies + if (json.Contains("\"Activated\":true")) + { + result.Activated = true; + } + + if (json.Contains("\"Dismissed\":true")) + { + result.Dismissed = true; + } + + // Extract button ID + int buttonIdStart = json.IndexOf("\"ClickedButtonId\":\""); + if (buttonIdStart >= 0) + { + buttonIdStart += "\"ClickedButtonId\":\"".Length; + int buttonIdEnd = json.IndexOf("\"", buttonIdStart); + if (buttonIdEnd > buttonIdStart) + { + result.ClickedButtonId = json.Substring(buttonIdStart, buttonIdEnd - buttonIdStart); + } + } + + // Extract button text + int buttonTextStart = json.IndexOf("\"ClickedButtonText\":\""); + if (buttonTextStart >= 0) + { + buttonTextStart += "\"ClickedButtonText\":\"".Length; + int buttonTextEnd = json.IndexOf("\"", buttonTextStart); + if (buttonTextEnd > buttonTextStart) + { + result.ClickedButtonText = json.Substring(buttonTextStart, buttonTextEnd - buttonTextStart); + } + } + + // Extract button argument + int buttonArgStart = json.IndexOf("\"ClickedButtonArgument\":\""); + if (buttonArgStart >= 0) + { + buttonArgStart += "\"ClickedButtonArgument\":\"".Length; + int buttonArgEnd = json.IndexOf("\"", buttonArgStart); + if (buttonArgEnd > buttonArgStart) + { + result.ClickedButtonArgument = json.Substring(buttonArgStart, buttonArgEnd - buttonArgStart); + } + } + + // Extract interaction time + int interactionTimeStart = json.IndexOf("\"InteractionTime\":\""); + if (interactionTimeStart >= 0) + { + interactionTimeStart += "\"InteractionTime\":\"".Length; + int interactionTimeEnd = json.IndexOf("\"", interactionTimeStart); + if (interactionTimeEnd > interactionTimeStart) + { + string timeStr = json.Substring(interactionTimeStart, interactionTimeEnd - interactionTimeStart); + if (DateTime.TryParse(timeStr, out DateTime interactionTime)) + { + result.InteractionTime = interactionTime; + } + } + } + + // Check for deferral + if (result.ClickedButtonId == "defer") + { + result.Deferred = true; + + // Extract deferral time + int deferTimeStart = json.IndexOf("\"DeferredUntil\":\""); + if (deferTimeStart >= 0) + { + deferTimeStart += "\"DeferredUntil\":\"".Length; + int deferTimeEnd = json.IndexOf("\"", deferTimeStart); + if (deferTimeEnd > deferTimeStart) + { + string timeStr = json.Substring(deferTimeStart, deferTimeEnd - deferTimeStart); + if (DateTime.TryParse(timeStr, out DateTime deferredUntil)) + { + result.DeferredUntil = deferredUntil; + } + } + } + } + + // Check for deadline reached + if (json.Contains("\"DeadlineReached\":true")) + { + result.DeadlineReached = true; + + // Extract deadline reached time + int deadlineTimeStart = json.IndexOf("\"DeadlineReachedTime\":\""); + if (deadlineTimeStart >= 0) + { + deadlineTimeStart += "\"DeadlineReachedTime\":\"".Length; + int deadlineTimeEnd = json.IndexOf("\"", deadlineTimeStart); + if (deadlineTimeEnd > deadlineTimeStart) + { + string timeStr = json.Substring(deadlineTimeStart, deadlineTimeEnd - deadlineTimeStart); + if (DateTime.TryParse(timeStr, out DateTime deadlineReachedTime)) + { + result.DeadlineReachedTime = deadlineReachedTime; + } + } + } + } + } + catch + { + // Ignore parsing errors } - - // 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) { + if (string.IsNullOrEmpty(text)) + { + return string.Empty; + } + return text .Replace("&", "&") .Replace("<", "<") @@ -372,272 +321,5 @@ namespace WindowsNotifications.Services .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 index 7da3e05..bb84c8c 100644 --- a/WindowsNotifications/Services/UserImpersonation.cs +++ b/WindowsNotifications/Services/UserImpersonation.cs @@ -1,280 +1,275 @@ 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 + /// Utility for impersonating users. /// - internal class UserImpersonation : IDisposable + internal static class UserImpersonation { + /// + /// Runs an action as the specified user. + /// + /// The return type of the action. + /// The name of the user to impersonate. + /// The action to run. + /// The result of the action. + public static T RunAsUser(string userName, Func action) + { + if (string.IsNullOrEmpty(userName)) + { + return action(); + } + + IntPtr userToken = IntPtr.Zero; + IntPtr duplicateToken = IntPtr.Zero; + WindowsImpersonationContext impersonationContext = null; + + try + { + // Get the token for the user + if (!GetSessionUserToken(ref userToken, userName)) + { + return action(); + } + + // Duplicate the token + if (!DuplicateToken(userToken, 2, ref duplicateToken)) + { + return action(); + } + + // Create a WindowsIdentity from the token + using (WindowsIdentity identity = new WindowsIdentity(duplicateToken)) + { + // Impersonate the user + impersonationContext = identity.Impersonate(); + + // Run the action + return action(); + } + } + finally + { + // Clean up + if (impersonationContext != null) + { + impersonationContext.Undo(); + } + + if (userToken != IntPtr.Zero) + { + CloseHandle(userToken); + } + + if (duplicateToken != IntPtr.Zero) + { + CloseHandle(duplicateToken); + } + } + } + + private static bool GetSessionUserToken(ref IntPtr token, string userName) + { + IntPtr wtsToken = IntPtr.Zero; + int sessionId = -1; + + try + { + // Find the session ID for the user + IntPtr serverHandle = IntPtr.Zero; + IntPtr sessionInfo = IntPtr.Zero; + int sessionCount = 0; + int retVal = WTSEnumerateSessions(serverHandle, 0, 1, ref sessionInfo, ref sessionCount); + + if (retVal != 0) + { + int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); + IntPtr currentSession = sessionInfo; + + for (int i = 0; i < sessionCount; i++) + { + WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure(currentSession, typeof(WTS_SESSION_INFO)); + currentSession = IntPtr.Add(currentSession, dataSize); + + if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive) + { + IntPtr buffer = IntPtr.Zero; + int bytesReturned = 0; + + if (WTSQuerySessionInformation(IntPtr.Zero, si.SessionID, WTS_INFO_CLASS.WTSUserName, out buffer, out bytesReturned) && bytesReturned > 1) + { + string sessionUserName = Marshal.PtrToStringAnsi(buffer); + WTSFreeMemory(buffer); + + if (string.Equals(sessionUserName, userName, StringComparison.OrdinalIgnoreCase)) + { + sessionId = si.SessionID; + break; + } + } + } + } + + WTSFreeMemory(sessionInfo); + } + + if (sessionId == -1) + { + return false; + } + + // Get the user token for the session + if (!WTSQueryUserToken(sessionId, ref wtsToken)) + { + return false; + } + + // Duplicate the token + if (!DuplicateTokenEx(wtsToken, TOKEN_ASSIGN_PRIMARY | TOKEN_ALL_ACCESS, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, ref token)) + { + return false; + } + + return true; + } + finally + { + if (wtsToken != IntPtr.Zero) + { + CloseHandle(wtsToken); + } + } + } + #region Win32 API + private const int TOKEN_ASSIGN_PRIMARY = 0x0001; + private const int TOKEN_DUPLICATE = 0x0002; + private const int TOKEN_IMPERSONATE = 0x0004; + private const int TOKEN_QUERY = 0x0008; + private const int TOKEN_QUERY_SOURCE = 0x0010; + private const int TOKEN_ADJUST_PRIVILEGES = 0x0020; + private const int TOKEN_ADJUST_GROUPS = 0x0040; + private const int TOKEN_ADJUST_DEFAULT = 0x0080; + private const int TOKEN_ADJUST_SESSIONID = 0x0100; + private const int TOKEN_ALL_ACCESS = TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE | TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT | TOKEN_ADJUST_SESSIONID; + + private enum WTS_INFO_CLASS + { + WTSInitialProgram, + WTSApplicationName, + WTSWorkingDirectory, + WTSOEMId, + WTSSessionId, + WTSUserName, + WTSWinStationName, + WTSDomainName, + WTSConnectState, + WTSClientBuildNumber, + WTSClientName, + WTSClientDirectory, + WTSClientProductId, + WTSClientHardwareId, + WTSClientAddress, + WTSClientDisplay, + WTSClientProtocolType, + WTSIdleTime, + WTSLogonTime, + WTSIncomingBytes, + WTSOutgoingBytes, + WTSIncomingFrames, + WTSOutgoingFrames, + WTSClientInfo, + WTSSessionInfo, + WTSSessionInfoEx, + WTSConfigInfo, + WTSValidationInfo, + WTSSessionAddressV4, + WTSIsRemoteSession + } + + private enum WTS_CONNECTSTATE_CLASS + { + WTSActive, + WTSConnected, + WTSConnectQuery, + WTSShadow, + WTSDisconnected, + WTSIdle, + WTSListen, + WTSReset, + WTSDown, + WTSInit + } + + private enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + private enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential)] + private struct WTS_SESSION_INFO + { + public int SessionID; + [MarshalAs(UnmanagedType.LPStr)] + public string pWinStationName; + public WTS_CONNECTSTATE_CLASS State; + } + + [DllImport("wtsapi32.dll", SetLastError = true)] + private static extern int 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", SetLastError = true)] + private static extern bool WTSQuerySessionInformation( + IntPtr hServer, + int sessionId, + WTS_INFO_CLASS wtsInfoClass, + out IntPtr ppBuffer, + out int pBytesReturned); + + [DllImport("wtsapi32.dll", SetLastError = true)] + private static extern bool WTSQueryUserToken( + int sessionId, + ref IntPtr phToken); + [DllImport("advapi32.dll", SetLastError = true)] - private static extern bool LogonUser( - string lpszUsername, - string lpszDomain, - string lpszPassword, - int dwLogonType, - int dwLogonProvider, - out IntPtr phToken); + private static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + int dwDesiredAccess, + IntPtr lpTokenAttributes, + SECURITY_IMPERSONATION_LEVEL impersonationLevel, + TOKEN_TYPE tokenType, + ref IntPtr phNewToken); [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(); + int SECURITY_IMPERSONATION_LEVEL, + ref IntPtr DuplicateTokenHandle); [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; + private static extern bool CloseHandle(IntPtr hHandle); #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 index 97aeab8..fe40a4c 100644 --- a/WindowsNotifications/Services/UserSessionManager.cs +++ b/WindowsNotifications/Services/UserSessionManager.cs @@ -1,85 +1,166 @@ using System; using System.Collections.Generic; -using System.Runtime.InteropServices; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Principal; -using System.Diagnostics; namespace WindowsNotifications.Services { /// - /// Manages user sessions and detects interactive sessions + /// Service for managing user sessions and impersonation. /// 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 + /// + /// Checks if the current process is running as SYSTEM. + /// + /// True if running as SYSTEM, false otherwise. + public bool IsRunningAsSystem() { - public int SessionId; - [MarshalAs(UnmanagedType.LPStr)] - public string pWinStationName; - public WTS_CONNECTSTATE_CLASS State; + using (WindowsIdentity identity = WindowsIdentity.GetCurrent()) + { + return identity.IsSystem; + } } + /// + /// Checks if there's an interactive user session. + /// + /// True if there's an interactive user session, false otherwise. + public bool HasInteractiveUserSession() + { + return GetInteractiveUserSessions().Count > 0; + } + + /// + /// Gets all interactive user sessions. + /// + /// A list of interactive user sessions. + public List GetInteractiveUserSessions() + { + List sessions = new List(); + + try + { + IntPtr serverHandle = IntPtr.Zero; + IntPtr sessionInfo = IntPtr.Zero; + int sessionCount = 0; + int retVal = WTSEnumerateSessions(serverHandle, 0, 1, ref sessionInfo, ref sessionCount); + + if (retVal != 0) + { + int dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO)); + IntPtr currentSession = sessionInfo; + + for (int i = 0; i < sessionCount; i++) + { + WTS_SESSION_INFO si = (WTS_SESSION_INFO)Marshal.PtrToStructure(currentSession, typeof(WTS_SESSION_INFO)); + currentSession = IntPtr.Add(currentSession, dataSize); + + if (si.State == WTS_CONNECTSTATE_CLASS.WTSActive) + { + string userName = GetUserNameFromSessionId(si.SessionID); + if (!string.IsNullOrEmpty(userName) && !userName.Equals("SYSTEM", StringComparison.OrdinalIgnoreCase)) + { + sessions.Add(userName); + } + } + } + + WTSFreeMemory(sessionInfo); + } + } + catch + { + // Ignore errors + } + + return sessions; + } + + /// + /// Runs an action as the interactive user. + /// + /// The return type of the action. + /// The action to run. + /// The result of the action. + public T RunAsInteractiveUser(Func action) + { + if (!IsRunningAsSystem()) + { + return action(); + } + + List sessions = GetInteractiveUserSessions(); + if (sessions.Count == 0) + { + return action(); + } + + return UserImpersonation.RunAsUser(sessions[0], action); + } + + private string GetUserNameFromSessionId(int sessionId) + { + IntPtr buffer = IntPtr.Zero; + int bytesReturned = 0; + string userName = string.Empty; + + try + { + if (WTSQuerySessionInformation(IntPtr.Zero, sessionId, WTS_INFO_CLASS.WTSUserName, out buffer, out bytesReturned) && bytesReturned > 1) + { + userName = Marshal.PtrToStringAnsi(buffer); + } + } + finally + { + if (buffer != IntPtr.Zero) + { + WTSFreeMemory(buffer); + } + } + + return userName; + } + + #region Win32 API + 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 + WTSInitialProgram, + WTSApplicationName, + WTSWorkingDirectory, + WTSOEMId, + WTSSessionId, + WTSUserName, + WTSWinStationName, + WTSDomainName, + WTSConnectState, + WTSClientBuildNumber, + WTSClientName, + WTSClientDirectory, + WTSClientProductId, + WTSClientHardwareId, + WTSClientAddress, + WTSClientDisplay, + WTSClientProtocolType, + WTSIdleTime, + WTSLogonTime, + WTSIncomingBytes, + WTSOutgoingBytes, + WTSIncomingFrames, + WTSOutgoingFrames, + WTSClientInfo, + WTSSessionInfo, + WTSSessionInfoEx, + WTSConfigInfo, + WTSValidationInfo, + WTSSessionAddressV4, + WTSIsRemoteSession } - public enum WTS_CONNECTSTATE_CLASS + private enum WTS_CONNECTSTATE_CLASS { WTSActive, WTSConnected, @@ -93,260 +174,34 @@ namespace WindowsNotifications.Services WTSInit } - private const int WTS_CURRENT_SERVER_HANDLE = 0; + [StructLayout(LayoutKind.Sequential)] + private struct WTS_SESSION_INFO + { + public int SessionID; + [MarshalAs(UnmanagedType.LPStr)] + public string pWinStationName; + public WTS_CONNECTSTATE_CLASS State; + } + + [DllImport("wtsapi32.dll", SetLastError = true)] + private static extern int 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", SetLastError = true)] + private static extern bool WTSQuerySessionInformation( + IntPtr hServer, + int sessionId, + WTS_INFO_CLASS wtsInfoClass, + out IntPtr ppBuffer, + out int pBytesReturned); #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/SimpleNotificationManager.cs b/WindowsNotifications/SimpleNotificationManager.cs deleted file mode 100644 index 3e5b1f3..0000000 --- a/WindowsNotifications/SimpleNotificationManager.cs +++ /dev/null @@ -1,228 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading; -using WindowsNotifications.Models; - -namespace WindowsNotifications -{ - /// - /// Simple implementation of the Windows Notifications library - /// - public class SimpleNotificationManager - { - private readonly string _databasePath; - private readonly Dictionary _reminderTimers = new Dictionary(); - - /// - /// Gets or sets the path to the database file - /// - public string DatabasePath { get; private set; } - - /// - /// Creates a new SimpleNotificationManager with the default database path - /// - public SimpleNotificationManager() : this(null) - { - } - - /// - /// Creates a new SimpleNotificationManager with the specified database path - /// - /// The path to the database file, or null to use the default - public SimpleNotificationManager(string databasePath) - { - DatabasePath = databasePath ?? Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "WindowsNotifications", "notifications.db"); - } - - /// - /// 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"); - - // Create a result - var result = new NotificationResult(options.Id) - { - Displayed = true, - CreatedTime = DateTime.Now - }; - - // Log the notification - if (options.EnableLogging) - { - string logEntry = $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Notification displayed: ID={options.Id}, Title={options.Title}"; - Debug.WriteLine(logEntry); - options.LogAction?.Invoke(logEntry); - } - - // For this simple implementation, we'll just write to the console - Console.WriteLine($"Notification: {options.Title}"); - Console.WriteLine($"Message: {options.Message}"); - - if (options.Buttons.Count > 0) - { - Console.WriteLine("Buttons:"); - foreach (var button in options.Buttons) - { - Console.WriteLine($"- {button.Text} (ID: {button.Id})"); - } - } - - // Simulate user interaction - if (!options.Async) - { - // Simulate a delay - Thread.Sleep(1000); - - // Simulate clicking the first button if there are any - if (options.Buttons.Count > 0) - { - var button = options.Buttons[0]; - result.Activated = true; - result.ClickedButtonId = button.Id; - result.ClickedButtonText = button.Text; - result.ClickedButtonArgument = button.Argument; - result.InteractionTime = DateTime.Now; - } - else - { - // Simulate dismissal - result.Dismissed = true; - result.InteractionTime = DateTime.Now; - } - } - - 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); - } - - /// - /// Gets the result of a notification - /// - /// The ID of the notification - /// The notification result, or null if not found - public NotificationResult GetNotificationResult(string notificationId) - { - // In this simple implementation, we always return null - 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) - { - // In this simple implementation, we always return null - return null; - } - - /// - /// Gets all notification results - /// - /// A list of notification results - public List GetAllNotificationResults() - { - // In this simple implementation, we always return an empty list - return new List(); - } - - /// - /// Deletes a notification result - /// - /// The ID of the notification - /// True if the deletion was successful, false otherwise - public bool DeleteNotificationResult(string notificationId) - { - // In this simple implementation, we always return true - return true; - } - - /// - /// Deletes all notification results - /// - /// True if the deletion was successful, false otherwise - public bool DeleteAllNotificationResults() - { - // In this simple implementation, we always return true - return true; - } - - /// - /// Gets the path to the database file - /// - /// The database file path - public string GetDatabaseFilePath() - { - return DatabasePath; - } - - /// - /// Checks if the current process is running as SYSTEM - /// - /// True if running as SYSTEM, false otherwise - public bool IsRunningAsSystem() - { - // In this simple implementation, we always return false - return false; - } - - /// - /// Gets all interactive user sessions - /// - /// A list of interactive user sessions - public List GetInteractiveUserSessions() - { - // In this simple implementation, we always return an empty list - return new List(); - } - } -} diff --git a/WindowsNotifications/SimpleWindowsNotifications.csproj b/WindowsNotifications/SimpleWindowsNotifications.csproj deleted file mode 100644 index e061938..0000000 --- a/WindowsNotifications/SimpleWindowsNotifications.csproj +++ /dev/null @@ -1,47 +0,0 @@ - - - - - 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 - - - - - - - - - - - - - - - - diff --git a/WindowsNotifications/Utils/AssemblyLoader.cs b/WindowsNotifications/Utils/AssemblyLoader.cs index d2b1d80..37c134d 100644 --- a/WindowsNotifications/Utils/AssemblyLoader.cs +++ b/WindowsNotifications/Utils/AssemblyLoader.cs @@ -5,88 +5,51 @@ using System.Reflection; namespace WindowsNotifications.Utils { /// - /// Provides utilities for loading assemblies in PowerShell + /// Utility for loading assemblies. /// - public static class AssemblyLoader + internal static class AssemblyLoader { /// - /// Gets the current assembly as a byte array + /// Gets the bytes of an embedded resource. /// - /// The assembly as a byte array - public static byte[] GetAssemblyBytes() + /// The name of the resource. + /// The bytes of the resource. + public static byte[] GetEmbeddedResourceBytes(string resourceName) { - try + Assembly assembly = Assembly.GetExecutingAssembly(); + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { - string assemblyPath = Assembly.GetExecutingAssembly().Location; - return File.ReadAllBytes(assemblyPath); - } - catch (Exception ex) - { - throw new Exception("Failed to get assembly bytes", ex); + if (stream == null) + { + throw new Exception($"Resource {resourceName} not found."); + } + + byte[] buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return buffer; } } /// - /// Gets the current assembly as a Base64 encoded string + /// Gets the text of an embedded resource. /// - /// The assembly as a Base64 encoded string - public static string GetAssemblyBase64() + /// The name of the resource. + /// The text of the resource. + public static string GetEmbeddedResourceText(string resourceName) { - try + Assembly assembly = Assembly.GetExecutingAssembly(); + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { - byte[] bytes = GetAssemblyBytes(); - return Convert.ToBase64String(bytes); + if (stream == null) + { + throw new Exception($"Resource {resourceName} not found."); + } + + using (StreamReader reader = new StreamReader(stream)) + { + return reader.ReadToEnd(); + } } - 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 index f8803ae..e28df82 100644 --- a/WindowsNotifications/Utils/LiteDBEmbedded.cs +++ b/WindowsNotifications/Utils/LiteDBEmbedded.cs @@ -1,259 +1,48 @@ using System; -using System.Collections.Generic; using System.IO; using System.Reflection; -using WindowsNotifications.Models; namespace WindowsNotifications.Utils { /// - /// Provides embedded LiteDB functionality + /// Utility for working with embedded LiteDB. /// - internal class LiteDBEmbedded + internal static 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; + private static readonly object _lockObject = new object(); + private static Assembly _liteDbAssembly; /// - /// Creates a new LiteDBEmbedded instance with the specified database path + /// Gets a LiteDB database instance. /// - /// The path to the database file - public LiteDBEmbedded(string dbPath) + /// The path to the database file. + /// A LiteDB database instance. + public static dynamic GetDatabase(string databasePath) { - _dbPath = dbPath; + EnsureLiteDbLoaded(); - // 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(); + Type liteDbType = _liteDbAssembly.GetType("LiteDB.LiteDatabase"); + return Activator.CreateInstance(liteDbType, databasePath); } - /// - /// Loads the embedded LiteDB assembly - /// - /// The LiteDB assembly - private Assembly LoadLiteDbAssembly() + private static void EnsureLiteDbLoaded() { - try + if (_liteDbAssembly != null) { - // Try to load from embedded resource - string resourceName = "WindowsNotifications.Resources.LiteDB.dll"; - using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName)) + return; + } + + lock (_lockObject) + { + if (_liteDbAssembly != null) { - if (stream != null) - { - byte[] assemblyData = new byte[stream.Length]; - stream.Read(assemblyData, 0, assemblyData.Length); - return Assembly.Load(assemblyData); - } + return; } - // If not found as embedded resource, try to load from file - return Assembly.LoadFrom(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "LiteDB.dll")); + // Load the embedded LiteDB assembly + byte[] liteDbBytes = AssemblyLoader.GetEmbeddedResourceBytes("WindowsNotifications.Resources.LiteDB.dll"); + _liteDbAssembly = Assembly.Load(liteDbBytes); } - 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 index a757791..1a536ed 100644 --- a/WindowsNotifications/WindowsNotifications.csproj +++ b/WindowsNotifications/WindowsNotifications.csproj @@ -33,29 +33,42 @@ - - False - C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Runtime.WindowsRuntime.dll - True - - - C:\Windows\Microsoft.NET\Framework\v4.0.30319\Windows.winmd - False - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - - @@ -63,8 +76,5 @@ - - -
diff --git a/build.ps1 b/build.ps1 index 8cd6149..846d89e 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,37 +1,79 @@ -# Build script for WindowsNotifications +# +# build.ps1 +# Build script for the Windows Notifications library +# + +param ( + [switch]$Clean, + [switch]$Release, + [switch]$Test +) # Set the configuration -$Configuration = "Release" +$configuration = if ($Release) { "Release" } else { "Debug" } -# Check if dotnet CLI is available -$dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -if (-not $dotnetPath) { - Write-Host "The .NET SDK is not installed or not in the PATH. Please install the .NET SDK." -ForegroundColor Red +# Set the MSBuild path +$msbuildPath = "C:\Windows\Microsoft.NET\Framework\v4.0.30319\MSBuild.exe" +if (-not (Test-Path $msbuildPath)) { + Write-Error "MSBuild not found at $msbuildPath" exit 1 } -# Clean the solution -Write-Host "Cleaning solution..." -ForegroundColor Cyan -dotnet clean WindowsNotifications.sln --configuration $Configuration - -# Restore NuGet packages -Write-Host "Restoring NuGet packages..." -ForegroundColor Cyan -dotnet restore WindowsNotifications.sln +# Clean the solution if requested +if ($Clean) { + Write-Host "Cleaning solution..." + & $msbuildPath "WindowsNotifications.sln" /t:Clean /p:Configuration=$configuration /v:minimal + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to clean solution" + exit 1 + } +} # Build the solution -Write-Host "Building solution..." -ForegroundColor Cyan -dotnet build WindowsNotifications.sln --configuration $Configuration --no-restore - -# 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 +Write-Host "Building solution in $configuration configuration..." +& $msbuildPath "WindowsNotifications.sln" /t:Build /p:Configuration=$configuration /v:minimal +if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to build solution" + exit 1 } + +# Run tests if requested +if ($Test) { + Write-Host "Running tests..." + $testDll = "WindowsNotifications.Tests\bin\$configuration\WindowsNotifications.Tests.dll" + if (-not (Test-Path $testDll)) { + Write-Error "Test DLL not found at $testDll" + exit 1 + } + + # Try to find NUnit console runner + $nunitPath = "packages\NUnit.ConsoleRunner\tools\nunit3-console.exe" + if (-not (Test-Path $nunitPath)) { + Write-Host "NUnit console runner not found, installing..." + & nuget install NUnit.ConsoleRunner -OutputDirectory packages + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to install NUnit console runner" + exit 1 + } + + $nunitPath = (Get-ChildItem -Path "packages" -Filter "nunit3-console.exe" -Recurse).FullName + if (-not $nunitPath) { + Write-Error "NUnit console runner not found after installation" + exit 1 + } + } + + & $nunitPath $testDll + if ($LASTEXITCODE -ne 0) { + Write-Error "Tests failed" + exit 1 + } +} + +# Copy the DLL to the PowerShell module directory +Write-Host "Copying DLL to PowerShell module directory..." +$dllPath = "WindowsNotifications\bin\$configuration\WindowsNotifications.dll" +$modulePath = "PowerShell\WindowsNotifications.dll" +Copy-Item -Path $dllPath -Destination $modulePath -Force + +Write-Host "Build completed successfully" diff --git a/check-assembly.ps1 b/check-assembly.ps1 new file mode 100644 index 0000000..92273d1 --- /dev/null +++ b/check-assembly.ps1 @@ -0,0 +1,3 @@ +Add-Type -Path 'WindowsNotifications\bin\Release\WindowsNotifications.dll' +$assembly = [AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.GetName().Name -eq 'WindowsNotifications' } +$assembly.GetTypes() | Select-Object FullName diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e854aa9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,9 @@ +# Documentation + +This directory contains documentation for the Windows Notifications project. + +## Contents + +- `architecture.md` - Overview of the system architecture +- `api.md` - API documentation +- `usage.md` - Usage examples and best practices diff --git a/run-tests.ps1 b/run-tests.ps1 deleted file mode 100644 index a8b5b09..0000000 --- a/run-tests.ps1 +++ /dev/null @@ -1,33 +0,0 @@ -# Run tests for SimpleWindowsNotifications - -# Set the configuration -$Configuration = "Release" - -# Check if dotnet CLI is available -$dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -if (-not $dotnetPath) { - Write-Host "The .NET SDK is not installed or not in the PATH. Please install the .NET SDK." -ForegroundColor Red - exit 1 -} - -# Build the solution -Write-Host "Building solution..." -ForegroundColor Cyan -dotnet build SimpleWindowsNotifications.sln --configuration $Configuration - -# Check if the build was successful -if ($LASTEXITCODE -ne 0) { - Write-Host "Build failed with exit code $LASTEXITCODE" -ForegroundColor Red - exit $LASTEXITCODE -} - -# Run the tests -Write-Host "Running tests..." -ForegroundColor Cyan -dotnet test SimpleWindowsNotifications.sln --configuration $Configuration --no-build - -# Check if the tests were successful -if ($LASTEXITCODE -eq 0) { - Write-Host "All tests passed!" -ForegroundColor Green -} else { - Write-Host "Tests failed with exit code $LASTEXITCODE" -ForegroundColor Red - exit $LASTEXITCODE -} diff --git a/simple-build.ps1 b/simple-build.ps1 deleted file mode 100644 index f80fdc0..0000000 --- a/simple-build.ps1 +++ /dev/null @@ -1,37 +0,0 @@ -# Build script for SimpleWindowsNotifications - -# Set the configuration -$Configuration = "Release" - -# Check if dotnet CLI is available -$dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -if (-not $dotnetPath) { - Write-Host "The .NET SDK is not installed or not in the PATH. Please install the .NET SDK." -ForegroundColor Red - exit 1 -} - -# Clean the solution -Write-Host "Cleaning solution..." -ForegroundColor Cyan -dotnet clean SimpleWindowsNotifications.sln --configuration $Configuration - -# Restore NuGet packages -Write-Host "Restoring NuGet packages..." -ForegroundColor Cyan -dotnet restore SimpleWindowsNotifications.sln - -# Build the solution -Write-Host "Building solution..." -ForegroundColor Cyan -dotnet build SimpleWindowsNotifications.sln --configuration $Configuration --no-restore - -# 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 -} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..3d4d337 --- /dev/null +++ b/src/README.md @@ -0,0 +1,8 @@ +# Source Code + +This directory contains the source code for the Windows Notifications project. + +## Structure + +- `NotificationApp/` - The WPF application for displaying notifications +- `PowerShell/` - PowerShell module for integrating with the notification system diff --git a/test-simple.ps1 b/test-simple.ps1 deleted file mode 100644 index 9e00b4f..0000000 --- a/test-simple.ps1 +++ /dev/null @@ -1,50 +0,0 @@ -# Test script for SimpleWindowsNotifications - -# 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) - Write-Host "Assembly loaded: $($assembly.FullName)" -ForegroundColor Green -} else { - Write-Error "WindowsNotifications.dll not found at $dllPath. Please build the solution first." - exit -} - -# Create a notification manager -$notificationManager = New-Object WindowsNotifications.SimpleNotificationManager -Write-Host "NotificationManager created" -ForegroundColor Green - -# Show a simple notification -Write-Host "Showing a simple notification..." -ForegroundColor Cyan -$result = $notificationManager.ShowSimpleNotification("Hello, World!", "This is a test notification from the SimpleWindowsNotifications library.") -Write-Host "Notification displayed: $($result.Displayed)" -ForegroundColor Yellow -Write-Host "Notification ID: $($result.NotificationId)" -ForegroundColor Yellow - -# Show a notification with buttons -Write-Host "`nShowing a notification with buttons..." -ForegroundColor Cyan -$result = $notificationManager.ShowNotificationWithButtons("Notification with Buttons", "Please select an option:", "OK", "Cancel") -Write-Host "Notification displayed: $($result.Displayed)" -ForegroundColor Yellow -Write-Host "Button clicked: $($result.ClickedButtonText)" -ForegroundColor Yellow - -# Show a custom notification -Write-Host "`nShowing a custom notification..." -ForegroundColor Cyan -$options = New-Object WindowsNotifications.Models.NotificationOptions -$options.Title = "Custom Notification" -$options.Message = "This is a custom notification with options." -$options.Async = $true -$options.EnableLogging = $true -$options.LogAction = { param($logEntry) Write-Host $logEntry -ForegroundColor Gray } - -$button1 = New-Object WindowsNotifications.Models.NotificationButton("Yes", "yes", "yes-arg") -$button2 = New-Object WindowsNotifications.Models.NotificationButton("No", "no", "no-arg") -$button3 = New-Object WindowsNotifications.Models.NotificationButton("Maybe", "maybe", "maybe-arg") -$options.Buttons.Add($button1) -$options.Buttons.Add($button2) -$options.Buttons.Add($button3) - -$result = $notificationManager.ShowNotification($options) -Write-Host "Notification displayed: $($result.Displayed)" -ForegroundColor Yellow -Write-Host "Notification ID: $($result.NotificationId)" -ForegroundColor Yellow - -Write-Host "`nTest completed successfully!" -ForegroundColor Green