ChocolateyUpdateMonitor.psm1


<#
.SYNOPSIS
    Retrieves the configuration settings for the Chocolatey Update Monitor.
 
.DESCRIPTION
    The Get-ChocoMonitorConfig function reads the settings from a JSON file located in a specific path. If the settings file does not exist, it generates default settings and saves them to the file. It then returns these settings.
 
.EXAMPLE
    Get-ChocoMonitorConfig
    This example retrieves the Chocolatey Update Monitor settings.
#>


function Get-ChocoMonitorConfig {
    [CmdletBinding()]
    param()

    Write-Verbose "Loading Chocolatey Update Monitor settings"

    if (-NOT(Test-Path "$env:ProgramData\ChocoUpdateMonitor\settings.json")) {
        Write-Verbose "No settings file found. Generating default settings..."

        $defaultSettings = @{
            UseRocolatey           = Test-Path "${env:ProgramFiles}\Rocolatey"
            AppIcon                = 'https://wdc.help/icons/Box.Packed.ico'
            ChocoSources           = @()
            AppTitle               = 'Chocolatey Update Monitor'
            UpdateCheckInterval    = "Daily"
            UpdateNotifyWebhookURL = ""
        }

        $chocoSources = choco source list -r | 
            ConvertFrom-Csv -Header 'SourceName', 'SourceUrl', 'disabled', 'Username', 'Password', 'Priority', 'BypassProxy', 'SelfService', 'AdminOnly' -delimiter '|' |
            Where-Object disabled -ne $true | # Exclude disabled sources
            Select-Object @{Name='SourceName'; Expression={$_.SourceName}}, @{Name='SourceUrl'; Expression={$_.SourceUrl}}

        foreach ($source in $chocoSources) {
            $defaultSettings.ChocoSources += @{
                SourceName = $source.SourceName
                SourceUrl  = $source.SourceUrl
            }
        }

        $defaultSettings | ConvertTo-Json -Depth 4 | Set-Content "$env:ProgramData\ChocoUpdateMonitor\settings.json"
    }

    Write-Verbose "Reading settings from file..."
    $settings = Get-Content "$env:ProgramData\ChocoUpdateMonitor\settings.json" | ConvertFrom-Json

    return $settings
}

<#
.SYNOPSIS
    Retrieves the list of outdated Chocolatey packages.
 
.DESCRIPTION
    The Get-ChocolateyUpdate function checks for outdated packages using either Chocolatey or Rocolatey, depending on the configuration settings. It fetches updates from the sources specified in the settings and saves the list of outdated packages to a JSON file.
 
.EXAMPLE
    Get-ChocolateyUpdate
    This example retrieves the list of outdated Chocolatey packages and saves it to a JSON file.
#>


function Get-ChocolateyUpdate {
    [CmdletBinding()]
    param()

    $settings = Get-ChocoMonitorConfig
    $outdatedPackages = @()

    if ($settings.UseRocolatey) {
        Write-Verbose "Using Rocolatey"

        $allChocoSources = choco source list -r | ConvertFrom-Csv -Header 'SourceName', 'SourceUrl', 'disabled', 'Username', 'Password', 'Priority', 'BypassProxy', 'SelfService', 'AdminOnly' -delimiter '|'

        $sourceNamesToDisable = $allChocoSources.SourceName | Where-Object { $_ -notin $settings.ChocoSources.SourceName }

        foreach ($thisChocoSource in $allChocoSources) {
            try {
                $request = Invoke-WebRequest -Uri $thisChocoSource.SourceURL -TimeoutSec 5

                if ($request.StatusCode -ne 200) {
                    Write-Verbose "Skipping source $($thisChocoSource.SourceURL) as it's not online."
                    $sourceNamesToDisable += $thisChocoSource.SourceName
                }    
            } catch {
                Write-Verbose "There was a problem connecting to $($thisChocoSource.SourceURL)."
            }


        }

        Write-Verbose "Temporarily disabling sources not specified in settings"
        $sourceNamesToDisable | ForEach-Object { choco source disable -n $_ }

        try {
            $outdatedPackages = roco outdated -r | 
                ConvertFrom-Csv -Delimiter '|' -Header 'Name', 'CurrentVersion', 'AvailableVersion', 'Pinned'
        } catch {
            Write-Verbose "There was a problem running Rocolatey."
        }finally {
            Write-Verbose "Re-enabling temporarily disabled sources"
            $sourceNamesToDisable | ForEach-Object { choco source enable -n $_ }
        }
    }
    else {
        Write-Verbose "Using Chocolatey"

        foreach ($source in $settings.ChocoSources) {
            try {
                $request = Invoke-WebRequest -Uri $source.SourceURL -TimeoutSec 5

                if ($request.StatusCode -eq 200) {
                    Write-Verbose "Retrieving updates from $($source.SourceURL)..."
                    $outdatedPackage = choco outdated --source $source.SourceURL -r --ignore-unfound | ConvertFrom-Csv -Delimiter '|' -Header 'Name', 'CurrentVersion', 'AvailableVersion', 'Pinned'
                    $outdatedPackage | add-member -NotePropertyName Source -NotePropertyValue $source.SourceURL
                    $outdatedPackages += $outdatedPackage
                } else {
                    Write-Verbose "Skipping source $($source.SourceURL) as it's not online."
                }    
            } catch {
                Write-Verbose "There was a problem connecting to $($source.SourceURL)."
            }
        }
    }

    Set-Content "$env:ProgramData\ChocoUpdateMonitor\updates.json" ($outdatedPackages | ConvertTo-Json -Depth 4)
}

<#
.SYNOPSIS
    Installs updates for outdated Chocolatey packages.
 
.DESCRIPTION
    The Install-ChocolateyUpdate function reads the list of outdated packages from a JSON file. It then iterates over each package and if the package is not pinned, it attempts to upgrade it using Chocolatey. If there are multiple updates for the same package, they are skipped to avoid version conflicts.
 
.EXAMPLE
    Install-ChocolateyUpdate
    This example installs updates for all outdated and unpinned Chocolatey packages listed in the JSON file.
#>


function Install-ChocolateyUpdate {
    [CmdletBinding(SupportsShouldProcess)]
    param ()

    $updatesFile = "$env:ProgramData\ChocoUpdateMonitor\updates.json"

    if (-NOT(Test-Path $updatesFile)) {
        Write-Verbose "No updates file found. Fetching the latest updates..."
        Get-ChocolateyUpdate
    }

    $updates = (Get-Content $updatesFile | ConvertFrom-Json)

    foreach ($update in $updates) {
        if ($update.Pinned -ne $true) {
            Write-Verbose "Installing update for $($update.Name)..."
            # Since Rocolatey does not support install or upgrade, we will always use Chocolatey for these operations.

            # If there are multiple updates for the same package, we need to skip all of them to avoid version conflicts.
            if(($updates | Where-Object { $_.Name -eq $update.Name }).Count -gt 1) {
                if($WhatIfPreference.IsPresent) {
                    Write-Output "WhatIf is enabled. Would have skipped $($update.Name) because there are multiple available source for this package."
                    continue
                } else {
                    Write-Warning "Package $($update.Name) is a duplicate. Skipping..."
                    continue
                }
            } else {
                if($WhatIfPreference.IsPresent) {
                    Write-Output "WhatIf is enabled. Running with -NOOP parameter."
                    choco upgrade $update.Name --source $update.Source -y --noop -r
                } else {
                    choco upgrade $update.Name --source $update.Source -y -r
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Displays the list of outdated Chocolatey packages.
 
.DESCRIPTION
    The Show-ChocolateyUpdate function reads the list of outdated packages from a JSON file and displays each package. If there are multiple updates for the same package, a warning is displayed.
 
.EXAMPLE
    Show-ChocolateyUpdate
    This example displays the list of outdated Chocolatey packages listed in the JSON file.
#>


function Show-ChocolateyUpdate {
    [CmdletBinding()]
    param()

    $updatesFile = "$env:ProgramData\ChocoUpdateMonitor\updates.json"

    if (-NOT(Test-Path $updatesFile)) {
        Write-Verbose "No updates file found. Fetching the latest updates..."
        Get-ChocolateyUpdate
    }

    $updates = (Get-Content $updatesFile | ConvertFrom-Json)

    foreach ($update in $updates) {
        if (($updates | Where-Object { $_.Name -eq $update.Name }).Count -gt 1) {
            Write-Output $update
            Write-Warning "Package $($update.Name) has multiple available sources for update."
        } else {
            Write-Output $update
        }
    }
}

<#
.SYNOPSIS
    Registers a scheduled task to check for Chocolatey package updates.
 
.DESCRIPTION
    The Register-ChocoMonitorTask function creates a new scheduled task that runs the Get-ChocolateyUpdate function at a specified interval. The interval is determined by the UpdateCheckInterval setting, which can be "Daily", "Weekly", "AtLogOn", or "AtStartup".
 
.EXAMPLE
    Register-ChocoMonitorTask
    This example registers a scheduled task to check for Chocolatey package updates according to the interval specified in the settings.
#>


function Register-ChocoMonitorTask {
    [CmdletBinding(SupportsShouldProcess)]
    param ()

    $settings = Get-ChocoMonitorConfig
    $validTriggers = @("Daily", "Weekly", "AtLogOn", "AtStartup")
    $TaskName = 'ChocolateyUpdateMonitor'

    if ($settings.UpdateCheckInterval -notin $validTriggers) {
        Write-Error "Invalid UpdateCheckInterval setting. Must be one of: $validTriggers"
        return
    }

    $action = New-ScheduledTaskAction -Execute 'Powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -Command "& {Get-ChocolateyUpdate}"'
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount

    switch ($settings.UpdateCheckInterval) {
        "Daily" {
            $trigger = New-ScheduledTaskTrigger -Daily -At 10am
        }
        "Weekly" {
            $trigger = New-ScheduledTaskTrigger -Weekly -At 10am -DaysOfWeek Monday
        }
        "AtLogOn" {
            $trigger = New-ScheduledTaskTrigger -AtLogOn
        }
        "AtStartup" {
            $trigger = New-ScheduledTaskTrigger -AtStartup
        }
    }

    $taskSettings = New-ScheduledTaskSettingsSet -DontStopIfGoingOnBatteries -StartWhenAvailable -AllowStartIfOnBatteries

    if ($WhatIfPreference.IsPresent) {
        Write-Output "WhatIf is enabled. Would have created Chocolatey Update Monitor scheduled task"
        Install-Module ChocolateyUpdateMonitor -Scope AllUsers -WhatIf
        return
    } else {
        Write-Verbose "Creating Chocolatey Update Monitor scheduled task"
        Install-Module ChocolateyUpdateMonitor -Scope AllUsers
        Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $taskSettings
    }
}

<#
.SYNOPSIS
    Unregisters the scheduled task for checking Chocolatey package updates.
 
.DESCRIPTION
    The Unregister-ChocoMonitorTask function removes the scheduled task named "Chocolatey Update Monitor".
 
.EXAMPLE
    Unregister-ChocoMonitorTask
    This example removes the scheduled task for checking Chocolatey package updates.
#>


function Unregister-ChocoMonitorTask {
    [CmdletBinding(SupportsShouldProcess)]
    param()

    $TaskName = 'ChocolateyUpdateMonitor'
    
    try {
        Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
        Write-Verbose "Successfully removed the scheduled task: $TaskName"
    }
    catch {
        Write-Warning "Failed to remove the scheduled task: $TaskName"
        Write-Error $_
    }
}

<#
.SYNOPSIS
    Sets the configuration settings for the Chocolatey Update Monitor.
 
.DESCRIPTION
    The Set-ChocoMonitorConfig function updates the settings for the Chocolatey Update Monitor and writes them to a JSON file. The settings that can be updated include UseRocolatey, AppIcon, ChocoSources, AppTitle, UpdateNotifyWebhookURL, and UpdateCheckInterval. If the UpdateCheckInterval is changed, the scheduled task for checking updates is also updated.
 
.PARAMETER UseRocolatey
    Specifies whether to use Rocolatey. Should be a boolean value.
 
.PARAMETER AppIcon
    The URL of the application icon.
 
.PARAMETER ChocoSources
    The list of Chocolatey sources.
 
.PARAMETER AppTitle
    The title of the application.
 
.PARAMETER UpdateNotifyWebhookURL
    The URL of the webhook to notify about updates.
 
.PARAMETER UpdateCheckInterval
    The interval for checking updates. Can be "Daily", "Weekly", "AtLogOn", or "AtStartup".
 
.EXAMPLE
    Set-ChocoMonitorConfig -UseRocolatey $false -AppIcon "https://newicon.com/icon.png" -UpdateCheckInterval "Weekly"
    This example sets UseRocolatey to false, changes the AppIcon, and sets the UpdateCheckInterval to "Weekly".
#>


function Set-ChocoMonitorConfig {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory=$false)]
        [bool]$UseRocolatey,
        [Parameter(Mandatory=$false)]
        [string]$AppIcon,
        [Parameter(Mandatory=$false)]
        [psobject]$ChocoSources,
        [Parameter(Mandatory=$false)]
        [string]$AppTitle,
        [Parameter(Mandatory=$false)]
        [string]$UpdateNotifyWebhookURL,
        [Parameter(Mandatory=$false)]
        [string]$UpdateCheckInterval
    )
    
    $settingsFile = "$env:ProgramData\ChocoUpdateMonitor\settings.json"
    
    if (Test-Path $settingsFile) {
        $currentSettings = Get-Content $settingsFile | ConvertFrom-Json
    }
    else {
        $currentSettings = Get-ChocoMonitorConfig
    }

    if ($PSBoundParameters.ContainsKey('UseRocolatey')) {
        $currentSettings.UseRocolatey = $UseRocolatey
    }
    if ($PSBoundParameters.ContainsKey('AppIcon')) {
        $currentSettings.AppIcon = $AppIcon
    }
    if ($PSBoundParameters.ContainsKey('ChocoSources')) {
        $currentSettings.ChocoSources = $ChocoSources
    }
    if ($PSBoundParameters.ContainsKey('AppTitle')) {
        $currentSettings.AppTitle = $AppTitle
    }
    if ($PSBoundParameters.ContainsKey('UpdateNotifyWebhookURL')) {
        $currentSettings.UpdateNotifyWebhookURL = $UpdateNotifyWebhookURL
    }
    if ($PSBoundParameters.ContainsKey('UpdateCheckInterval')) {
        $currentSettings.UpdateCheckInterval = $UpdateCheckInterval
    }

    if ($WhatIfPreference.IsPresent) {
        Write-Output "WhatIf is enabled. Would have updated the settings file"
        return
    } else {
        $currentSettings | ConvertTo-Json | Set-Content $settingsFile
        Write-Verbose "Settings updated"
    }

    
    if ($UpdateCheckInterval -ne $currentSettings.UpdateCheckInterval) {
        if ($WhatIfPreference.IsPresent) {
            Write-Output "WhatIf is enabled. Would have updated the scheduled task"
            return
        } else {
            Write-Verbose "UpdateCheckInterval changed, updating the scheduled task..."
            Unregister-ChocoMonitorTask
            Register-ChocoMonitorTask
            }
        }
}


Export-ModuleMember -Function Get-ChocoMonitorConfig, Get-ChocolateyUpdate, Show-ChocolateyUpdate, Install-ChocolateyUpdate, Register-ChocoMonitorTask, Set-ChocoMonitorConfig, Unregister-ChocoMonitorTask