Private/Misc.ps1
|
function Format-PiHoleSecond { param ( [int]$TimeInSeconds ) $Data = [math]::Round($TimeInSeconds) $Object = [PSCustomObject]@{ TimeInSeconds = $Data } $ObjectFinal = $Object Write-Output $ObjectFinal } function Convert-PiHoleUnixTimeToLocalTime { param ( [int]$UnixTime ) $ConvertedTime = [timezone]::CurrentTimeZone.ToLocalTime(([datetime]'1/1/1970').AddSeconds($UnixTime)) $ObjectFinal = @() $Object = [PSCustomObject]@{ LocalTime = $ConvertedTime UnixTime = $UnixTime } $ObjectFinal = $Object Write-Output $ObjectFinal } function Convert-LocalTimeToPiHoleUnixTime { param ( [datetime]$Date ) $ConvertedTime = [int64](Get-Date $Date -UFormat %s) $Object = [PSCustomObject]@{ LocalTime = $Date UnixTime = $ConvertedTime } $ObjectFinal = $Object Write-Output $ObjectFinal } function ConvertTo-PiHolePascalCase { #INTERNAL FUNCTION param ( [string]$Name ) if ([string]::IsNullOrEmpty($Name)) { return $Name } $Segments = $Name -split '_' | Where-Object { $_.Length -gt 0 } $PascalSegments = foreach ($Segment in $Segments) { $Segment.Substring(0, 1).ToUpper() + $Segment.Substring(1) } return ($PascalSegments -join '') } function ConvertTo-PiHolePascalCaseObject { #INTERNAL FUNCTION # # Recursively rebuilds an API response as nested PSCustomObjects/arrays with PascalCase # property names (e.g. EXTERNAL_BLOCKED_IP / app_pwhash -> ExternalBlockedIp / AppPwhash), # so deep/wide response trees don't need every field hardcoded by hand to be PowerShell # object friendly - and so newly added API fields show up automatically instead of being # silently dropped. param ( [Parameter(ValueFromPipeline = $true)] $InputObject ) process { if ($null -eq $InputObject) { return $null } if ($InputObject -is [System.Management.Automation.PSCustomObject]) { $Result = [ordered]@{} foreach ($Prop in $InputObject.PSObject.Properties) { $Key = ConvertTo-PiHolePascalCase -Name $Prop.Name $Result[$Key] = ConvertTo-PiHolePascalCaseObject -InputObject $Prop.Value } return [PSCustomObject]$Result } if (($InputObject -is [System.Collections.IEnumerable]) -and ($InputObject -isnot [string])) { return @($InputObject | ForEach-Object { ConvertTo-PiHolePascalCaseObject -InputObject $_ }) } return $InputObject } } function ConvertTo-PiHoleFriendlyErrorMessage { #INTERNAL FUNCTION # # Pi-hole's own error responses often carry a clearer message/hint than the generic HTTP # exception text (e.g. "Unable to change configuration (read-only): ...app_sudo is false" # vs just "403 Forbidden"). This extracts and combines them when present, and adds a # concrete pointer to fix the most common cause of a blocked config write - the app # password's app_sudo setting - since Pi-hole's own hint says what's wrong but not how to # fix it. [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingEmptyCatchBlock", "", Justification = "Falls back to the raw exception message when the response body isn't valid JSON - nothing to report.")] param ( [Parameter(Mandatory = $true)] $ErrorRecord ) $Message = $ErrorRecord.Exception.Message if ($ErrorRecord.ErrorDetails.Message) { try { $ApiError = ($ErrorRecord.ErrorDetails.Message | ConvertFrom-Json).error if ($ApiError.message) { $Message = $ApiError.message if ($ApiError.hint) { $Message += ": $($ApiError.hint)" } if ($ApiError.hint -like '*app_sudo*') { $Message += " Enable it in your Pi-hole admin UI under Settings > All Settings by searching for 'app_sudo' and setting webserver.api.app_sudo to true." } } } catch { # ErrorDetails.Message wasn't valid JSON - fall back to the raw exception message } } return $Message } function Remove-PiHoleCurrentAuthSession { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Justification = "It removes sessions from PiHole only")] [CmdletBinding()] param ( [System.URI]$PiHoleServer, [string]$Sid, [bool]$IgnoreSsl = $false ) $Params = @{ Headers = @{sid = $($Sid) } Uri = "$($PiHoleServer.OriginalString)/api/auth" Method = "Delete" SkipCertificateCheck = $IgnoreSsl ContentType = "application/json" } try { $null = Invoke-RestMethod @Params } catch { # Best-effort logout, called from every public function's finally block - a transient # failure here (e.g. the server briefly unreachable right after a restart) must never # fail the caller. Write-Error would do exactly that under $ErrorActionPreference = # 'Stop', which Azure Pipelines' pwsh task sets by default. Write-Warning -Message "Failed to close Pi-hole session: $($_.Exception.Message)" } } |