public/Receive-NtfyPush.ps1

<#
.SYNOPSIS
    Queries for push notifications from a Ntfy server.
.DESCRIPTION
    Essentially a wrapper around the Ntfy Subscribe API to query for notifications
    from a specified topic on a Ntfy server. Queries are passed in as HTTP Headers
    via the -Parameters parameter.

    Note that this function automatically adds 'poll=1' to the parameters to
    immediately return results and removes any duplicates of it (and it's aliases).
    If you want to 'subscribe' over a period of time to new messages, you should
    use a different method.
.PARAMETER NtfyEndpoint
    The base URI of the Ntfy server.
.PARAMETER Topic
    The topic to query for notifications.
.PARAMETER Parameters
    HTTP Headers to use as query parameters.
    Preferred over URL Queries, especially for larger/more complex queries.
.PARAMETER Credential
    A PSCredential object containing the username and password for Basic Authentication.
.PARAMETER AccessToken
    A SecureString containing the access token for Bearer or Basic Authentication.
.PARAMETER TokenType
    The type of token provided in AccessToken. Valid values are 'Bearer' and 'Basic'.
    Defaults to 'Bearer'.
.LINK
    https://docs.ntfy.sh/subscribe/api
.LINK
    https://docs.ntfy.sh/subscribe/api/#json-message-format
.LINK
    https://docs.ntfy.sh/subscribe/api/#list-of-all-parameters
.EXAMPLE
    Receive-NtfyPush -NtfyEndpoint "https://ntfy.sh" -Topic "test"
.EXAMPLE
    Receive-NtfyPush -Parameters @{since='2h'} `
        -NtfyEndpoint "https://ntfy.sh" -Topic "test"
.NOTES
    This function automatically adds 'poll=1' to the parameters to immediately return results.
    If you want to 'subscribe' over a period of time to new messages, you should use a different method.
#>

function Receive-NtfyPush {
    [OutputType([System.Collections.Generic.List[PSCustomObject]])]
    [Alias('Receive-Ntfy')][Alias('rcn')]
    [CmdletBinding(DefaultParameterSetName = 'default')]
    param (
        [Parameter(Mandatory = $true)]
        [Uri]$NtfyEndpoint,
        [Parameter(Mandatory = $true)]
        [string]$Topic,

        [Parameter(Mandatory = $false)]
        [Hashtable]$Parameters = @{},

        [Parameter(ParameterSetName = 'Credential')]
        [PSCredential]$Credential = $null,

        [Parameter(ParameterSetName = 'AccessToken')]
        [SecureString]$AccessToken = $null,

        [Parameter(ParameterSetName = 'AccessToken')]
        [Parameter(ParameterSetName = 'Credential')]
        [ValidateSet("Bearer","Basic")]
        [string]$TokenType = "Bearer"
    )

    Write-Verbose "Custom Query parameters provided: $($Parameters.Count)"

    # build uri
    try {
        $builder = [System.UriBuilder]$NtfyEndpoint
        $builder.Path = (Join-Path -Path $builder.Path -ChildPath "$Topic/json")
        $FullUri = $builder.Uri.AbsoluteUri
    } catch {
        Write-TerminatingError -Exception $_.Exception `
            -Message "Failed to construct a properly formed Endpoint URI." `
            -Category InvalidData `
            -ErrorId "Ntfy.EndpointURIError"
    }

    # initial payload and headers
    $Headers = @{}
    $Payload = @{
        Method = "Get"
        Uri    = $FullUri
    }

    # build out access payload from Save-NtfyAuthentication
    switch($PSCmdlet.ParameterSetName) {
        'AccessToken' {
            Write-Verbose "Using AccessToken for authentication with TokenType: $TokenType"
            Save-NtfyAuthentication -Payload $Payload -Headers $Headers -AccessToken $AccessToken -TokenType $TokenType
        }
        'Credential' {
            Write-Verbose "Using PSCredential for Basic authentication"
            Save-NtfyAuthentication -Payload $Payload -Headers $Headers -Credential $Credential
        }
        default {
            Write-Verbose "No authentication method specified - Proceeding Anonymously."
        }
    }

    # Join Query Parameters into Headers
    foreach ($key in $Parameters.Keys) {
        Add-ObjectPropSafe -Object $Headers -Key $key -Value $Parameters[$key]
        Write-Debug "Added query parameter: $key = $($Parameters[$key])"
    }
    try {
        # ensure no duplicate poll parameter so we can always ensure it's set.
        'poll','po','X-Poll' | ForEach-Object {
            if ($Headers.ContainsKey($_)) {
                Write-Warning "The '$_' parameter is managed by Receive-NtfyPush and will be overridden to ensure immediate response. You may remove it from your Parameters hashtable."
                $Headers.Remove($_)
                Write-Verbose "Removing '$_' query parameter."
            }
        }
    } finally {
        Add-ObjectPropSafe -Object $Headers -Key 'poll' -Value 1
        Write-Verbose "Adding 'poll=1' query parameter."
    }

    # Join Headers into Payload
    Add-ObjectPropSafe -Object $Payload -Key "Headers" -Value $Headers

    # Receive Response
    Write-Verbose "Executing query to Ntfy server..."
    $response = Invoke-RestMethod @Payload
    $results = [System.Collections.Generic.List[PSCustomObject]]::new()

    # Determine response type and parse accordingly
    try {
        $ResponseTypeName = $response.GetType().Name
        Write-Debug "Raw response type: $ResponseTypeName"
        $JSONParsed = switch ($ResponseTypeName){
            'String' {
                Write-Debug "Processing newline-delimited JSON response"
                # Newline-delimited JSON response
                $response -split "`n" | ForEach-Object {
                    if ($_.Trim() -ne "") { # skip empty lines
                        try {
                            Write-Debug "Parsing JSON line: $_"
                            $_ | ConvertFrom-Json
                        } catch {
                            # Re-throw to trigger the outer catch and terminating error
                            throw "Failed to parse JSON line: $($_.Exception.Message)"
                        }
                    }
                } | Where-Object { $_ -ne $null } # filter out nulls
                break;
            }
            'PSCustomObject' {
                Write-Debug "Processing single object response"
                $response
                break;
            }
            default{}
        }
    }
    catch {
        Write-TerminatingError -Exception $_.Exception `
            -Message "Failed to parse the response from Ntfy." `
            -Category InvalidData `
            -ErrorId "Ntfy.ResponseParseError"
    }

    Write-Verbose "Processing $($JSONParsed.Count) notification entries"
    $JSONParsed | ForEach-Object {
        try {
            if(-not $_.id){
                Write-Debug "Skipping an entry without ID"
                continue # skip invalid entries
            }

            # cast tags to string array
            $Tags = if($null -ne $_.tags -and $_.tags -ne ""){
                [string[]]$_.tags
            } else {
                [string[]]@()
            }

            # format attachment
            $Attachment = if ($null -ne $_.attachment){
                [PSCustomObject]@{
                    Name    = $_.attachment.name
                    Type    = $_.attachment.type
                    Url     = $_.attachment.url
                    Size    = $_.attachment.size
                    Expires = Get-Date -Date ([DateTimeOffset]::FromUnixTimeSeconds($_.attachment.expires).DateTime) -ErrorAction SilentlyContinue
                }
            }

            # add the object to the list
            $results.Add([PSCustomObject]@{
                Id          = $_.id
                Title       = $_.title
                Message     = $_.message
                Priority    = $_.priority
                Tags        = [string[]]$Tags
                Attachment  = $Attachment
                Click       = $_.click
                Actions     = $_.actions
                Time        = Get-Date -Date ([DateTimeOffset]::FromUnixTimeSeconds($_.time).DateTime)
                Expires     = Get-Date -Date ([DateTimeOffset]::FromUnixTimeSeconds($_.expires).DateTime) -ErrorAction SilentlyContinue
                Icon        = $_.icon
            })
        } catch {
           Write-Error -Message "Failed to parse a notification entry from the response."
        }
    }

    Write-Verbose "Successfully retrieved $($results.Count) notifications"
    return $results
}
Set-Alias -Name rcn -Value Receive-NtfyPush
Set-Alias -Name Receive-Ntfy -Value Receive-NtfyPush