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 213eeec..14798bb 100644 Binary files a/WindowsNotifications/Resources/LiteDB.dll and b/WindowsNotifications/Resources/LiteDB.dll differ diff --git a/WindowsNotifications/Services/DatabaseService.cs b/WindowsNotifications/Services/DatabaseService.cs index 7b638ab..3a5e6d1 100644 --- a/WindowsNotifications/Services/DatabaseService.cs +++ b/WindowsNotifications/Services/DatabaseService.cs @@ -1,64 +1,61 @@ using System; using System.Collections.Generic; using System.IO; -using System.Reflection; +using System.Linq; using WindowsNotifications.Models; using WindowsNotifications.Utils; namespace WindowsNotifications.Services { /// - /// 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