utils.ps1

Function ConvertFrom-AnsiEscapedString {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline=$true, Position=0)]
        [string]$InputValObject
    )

    begin {
        $escChar = [char]27

        function Convert-Escapes {
            param([string]$s)
            if ($null -eq $s) { return $s }

            # Protect fenced triple-backtick blocks first
            $literals = New-Object System.Collections.Generic.List[string]
            $tokenIndex = 0

            # newlines and returns
            $s = $s -replace '(?<!\\)\\n', "`n"
            $s = $s -replace '(?<!\\)\\r', "`r"

            # new lines are messed up
            # # Protect ```...``` (multiline, non-greedy)
            # $s = [Regex]::Replace($s, '```([\s\S]*?)```', {
            # param($m)
            # $literals.Add($m.Value) # store the whole fenced block including backticks
            # $token = "___ANSI_LITERAL_{0}___" -f $tokenIndex
            # $tokenIndex++
            # return $token
            # })

            # Then protect single-backtick inline spans `...` (no newlines)
            $s = [Regex]::Replace($s, '`([^`\r\n]*)`', {
                param($m)
                $literals.Add($m.Value)            # store with backticks
                $token = "___ANSI_LITERAL_{0}___" -f $tokenIndex
                $tokenIndex++
                return $token
            })

            # \x5c033 -> ESC
            $s = $s -replace '\\x5c0*33', $escChar

            # common escape forms
            $s = $s -replace '\\033', $escChar
            $s = $s -replace '\\e', $escChar
            $s = $s -replace '\\x1b', $escChar
            $s = $s -replace '\\x1B', $escChar


            # replace generic \xHH sequences safely
            while ($s -match '\\x([0-9A-Fa-f]{2})') {
                $hex = $matches[1]
                $char = [char]([Convert]::ToInt32($hex,16))
                $s = [Regex]::Replace($s, "\\x$hex", [Regex]::Escape($char), 1)
                $s = $s -replace [Regex]::Escape($char), $char
            }

            # Restore literal spans (tokens replaced with original stored values)
            for ($i = 0; $i -lt $literals.Count; $i++) {
                $token = "___ANSI_LITERAL_{0}___" -f $i
                $orig = $literals[$i]
                # Use Regex::Replace with escaped token -> escaped original, then unescape inserted original
                $s = [Regex]::Replace($s, [Regex]::Escape($token), [Regex]::Escape($orig))
                $s = $s -replace [Regex]::Escape($orig), $orig
            }

            return $s
        }

        $collected = ""
    }

    process {
        $inputVal = $InputValObject
        if ($inputVal -is [System.Array]) {
            $inputVal = ($inputVal -join "`n")
        } elseif ($null -ne $inputVal -and $inputVal -isnot [string]) {
            $inputVal = $inputVal.ToString()
        }

        $out = Convert-Escapes -s $inputVal

        # if ($null -ne $out) {
        # Write-Host $out -noNewline
        # if ($out -match "(`r`n|`n)$") { Write-Host "" }
        # }

        $collected += $out
    }

    end {
        return $collected
    }
}

Function Get-PollinationsAiByokWeb {
    [CmdletBinding()]
    param(
        [string]
        [Parameter(Mandatory=$false, ParameterSetName='None')]
        [Parameter(Mandatory=$false, ParameterSetName='Add')]
        [Parameter(Mandatory=$false, ParameterSetName='Init')]
        $Url = "http://localhost:8888/",
        
        [string]
        [Parameter(Mandatory=$false, ParameterSetName='None')]
        [Parameter(Mandatory=$false, ParameterSetName='Add')]
        [Parameter(Mandatory=$false, ParameterSetName='Init')]
        [Alias("key")]
        $AppKey = "pk_2ZpluqiajXYP5XfG",

        [switch]
        [Parameter(Mandatory=$true, ParameterSetName='Add')]
        $Add = $false, # Add the API Key to the profile and init within the current environment

        [switch]
        [Parameter(Mandatory=$true, ParameterSetName='Init')]
        $Init = $false # if not added, still init the API key within the current environment
    )

    # 1. Setup the listener
    $listener = New-Object System.Net.HttpListener
    $listener.Prefixes.Add($Url)

    try {
        $listener.Start()
        Write-Debug "Server started on $Url"
    }
    catch {
        Write-Error "Failed to start the server. Port already in use?"
        $port = ([uri]$Url).Port
        netstat -ano | findstr ":$port"
        throw $_
    }

    # 2. Open the browser
    $authUrl = "https://enter.pollinations.ai/authorize?redirect_url={0}&app_key={1}" -f [System.Web.HttpUtility]::UrlEncode($Url), $AppKey
    Start-Process $authUrl


    $apiKey = $null
    $canceled = $null

    try {    
        # 2. Loop until we get the api_key query parameter
        Write-Host "Waiting for API Key... Press ESC to cancel." -ForegroundColor Yellow

        while ($null -eq $apiKey -and $null -eq $canceled) {

            $asyncResult = $listener.BeginGetContext($null, $null)
            
            while (-not $asyncResult.AsyncWaitHandle.WaitOne(50)) {

                if ([System.Console]::KeyAvailable) { # always true: $Host.UI.RawUI.KeyAvailable
                    $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
                    # on esc
                    if ($key.Character -eq [ConsoleKey]::Escape) {
                        $canceled = $true
                        break
                    }
                }
            }
            if ($canceled) {
                Write-Debug "Canceled by user."
                break
            }
            $context = $listener.EndGetContext($asyncResult)

            $request = $context.Request
            $apiKey = $request.QueryString["api_key"]
            $canceled = $request.QueryString["canceled"]
            $response = $context.Response
            $response.ContentType = "text/html"

            if ($null -ne $canceled) {
                $html = @"
<html style="display: grid; place-items: center;"><head><title>API Key - Pollinations AI</title></head><body style=" height: fit-content; font-family: sans-serif; ">
    <h1>Canceled!</h1>
    <p>API Key <b>NOT captured</b>. You can close the window.</p>
    <script>setTimeout(function() { window.close(); }, 1000);</script>
</body></html>
"@

                Write-Debug "Canceled"
            }
            elseif ($null -eq $apiKey) {
                # Serve the JS redirector
                $html = @"
<html style="display: grid; place-items: center;"><head><title>API Key - Pollinations AI</title></head><body style=" height: fit-content; font-family: sans-serif; ">
    <p>Processing...</p>
    <script>
        if (window.location.hash) {
            var hash = window.location.hash.substring(1);
            var params = new URLSearchParams(hash);
            var key = params.get('api_key');
            if (key) window.location.href = window.location.origin + '/?api_key=' + key;
        }
        else window.location.href = window.location.origin + '/?canceled=true';
    </script>
</body></html>
"@

            } else {
                # Serve the Success page
                $html = @"
<html style="display: grid; place-items: center;"><head><title>API Key - Pollinations AI</title></head><body style=" height: fit-content; font-family: sans-serif; ">
    <h1>Success!</h1>
    <p>API Key captured. You can close the window.</p>
    <script>setTimeout(function() { window.close(); }, 1000);</script>
</body></html>
"@

                Write-Debug "Successfully captured API Key: $apiKey"
            }

            $buffer = [System.Text.Encoding]::UTF8.GetBytes($html)
            $response.ContentLength64 = $buffer.Length
            $response.OutputStream.Write($buffer, 0, $buffer.Length)
            $response.Close()
        }
    }
    catch {
        $err = $_
        Write-Error "Error: $_"
    }
    finally {
        if ($listener.IsListening) { $listener.Stop() }
        $listener.Close()
        Write-Debug "Server stopped."

        if ($null -ne $err) {
            throw $err
        }
    }

    if ($Add) {
        if ($null -ne $apiKey) {   
            "`n`n`$env:POLLINATIONSAI_API_KEY = `"$($apiKey)`"" >> $PROFILE.CurrentUserAllHosts
            Write-Host "API Key added as `$env:POLLINATIONSAI_API_KEY to $($PROFILE.CurrentUserAllHosts)" -ForegroundColor Green
        }
        else {
            Write-Error "API Key not added to environment. Failed to capture API Key."
        }
    }
    if ($null -ne $apiKey -and ($Add -or $Init) ) {
        $env:POLLINATIONSAI_API_KEY = $apiKey # activate in session
    }

    return $apiKey
}




# https://datatracker.ietf.org/doc/html/rfc8628
# https://enter.pollinations.ai/api/docs#tag/-bring-your-own-pollen -> 🖥️ CLIs & Headless Apps (Device Flow)
# .Alias Get-PollinationsAiDeviceToken
function Get-PollinationsAiByok {
    param(
        [string]
        [Parameter(Mandatory=$false, ParameterSetName='None')]
        [Parameter(Mandatory=$false, ParameterSetName='Add')]
        [Parameter(Mandatory=$false, ParameterSetName='Init')]
        [Alias("AppKey")]
        [Alias("Key")]
        $ClientId = "pk_2ZpluqiajXYP5XfG",

        [switch]
        [Parameter(Mandatory=$true, ParameterSetName='Add')]
        $Add = $false, # Add the API Key to the profile and init within the current environment

        [switch]
        [Parameter(Mandatory=$true, ParameterSetName='Init')]
        $Init = $false, # if not added, still init the API key within the current environment


        [int]
        [Parameter(Mandatory=$false, ParameterSetName='None')]
        [Parameter(Mandatory=$false, ParameterSetName='Add')]
        [Parameter(Mandatory=$false, ParameterSetName='Init')]
        $TimeoutSeconds = 0,

        [int]
        [Parameter(Mandatory=$false, ParameterSetName='None')]
        [Parameter(Mandatory=$false, ParameterSetName='Add')]
        [Parameter(Mandatory=$false, ParameterSetName='Init')]
        $IntervalSeconds = 1
    )

    $BaseUrl = "https://enter.pollinations.ai/api/device"

    # initiate device code request - get handshake token and activation dialog url
    Write-Host "Requesting device code..." -ForegroundColor Cyan
    $deviceInfo = Invoke-RestMethod -Uri "$BaseUrl/code" -Method Post -Body (@{
        client_id = $ClientId
        scope     = "generate"
    } | ConvertTo-Json) -ContentType "application/json"

    # extra info for the user
    Write-Host "`nVisit: $($deviceInfo.verification_uri_complete)" -ForegroundColor Yellow
    Write-Host "Code: $($deviceInfo.user_code)" -ForegroundColor Green
    Write-Host "Waiting for confirmation... Press ESC to cancel." -ForegroundColor Gray

    
    # open browser with device code and pollen confirmation
    Start-Process $deviceInfo.verification_uri_complete

    $accessToken = $null
    $canceled = $false
    $lastRequestTime =[DateTime]::MinValue
    $startTime = Get-Date
    $intervalSeconds = if ($IntervalSeconds -gt 0) { $IntervalSeconds } else { [double]$deviceInfo.interval }
    $expirationTime = if ($TimeoutSeconds -gt 0) { $TimeoutSeconds } else { [double]$deviceInfo.expires_in }

    # waiting for API
    while ($null -eq $accessToken) {
        $currentTime = Get-Date

        # check for total expiration
        if (($currentTime - $startTime).TotalSeconds -gt $expirationTime) {
            Write-Warning "`nAuthorization request timed out after $expirationTime seconds."
            return $null
        }

        # check for ESC key
        if ([System.Console]::KeyAvailable) { # always true: $Host.UI.RawUI.KeyAvailable
            $key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
            # on esc
            if ($key.Character -eq [ConsoleKey]::Escape) {
                $canceled = $true
                break
            }
        }

        # API polling
        if (($currentTime - $lastRequestTime).TotalSeconds -ge $intervalSeconds) {
            $lastRequestTime = Get-Date 

            try {
                $tokenResponse = Invoke-RestMethod -Uri "$BaseUrl/token" -Method Post -Body (@{
                    device_code = $deviceInfo.device_code
                } | ConvertTo-Json) -ContentType "application/json" -ErrorAction Stop
                
                $accessToken = $tokenResponse.access_token
            }
            catch {
                # handle HTTP error responses
                if ($_.ErrorDetails -and $_.ErrorDetails.Message) {
                    try {
                        # Parse the error JSON
                        $errorData = $_.ErrorDetails.Message | ConvertFrom-Json
                        
                        if ($errorData.error -eq "access_denied") {
                            Write-Host "" # newline, since warning eats it
                            Write-Warning "Access denied by the user."
                            return $null
                        }
                        elseif ($errorData.error -eq "expired_token") {
                            Write-Error "`nThe device code has expired."
                            return $null
                        }
                        elseif ($errorData.error -eq "authorization_pending" -or $errorData.error.code -eq "NOT_FOUND") {
                            # Expected states while waiting for user action. Do nothing.
                        }
                    }
                    catch {
                        # JSON parsing failed, safely ignore and continue polling
                    }
                }
            }
        }

        # keep responsiveness high and CPU usage low
        Start-Sleep -Milliseconds 100
    }

    if ($canceled) {
        Write-Host "`nOperation canceled by user." -ForegroundColor Red
        return $null
    }

    if ($accessToken) {
        Write-Host "`nSuccessfully retrieved access token!" -ForegroundColor Green
    }

    if ($Add) {
        if ($null -ne $accessToken) {   
            "`n`n`$env:POLLINATIONSAI_API_KEY = `"$($accessToken)`"" >> $PROFILE.CurrentUserAllHosts
            Write-Host "API Key added as `$env:POLLINATIONSAI_API_KEY to $($PROFILE.CurrentUserAllHosts)" -ForegroundColor Green
        }
        else {
            Write-Error "API Key not added to environment. Failed to retrieve access token."
        }
    }
    if ($null -ne $accessToken -and ($Add -or $Init) ) {
        $env:POLLINATIONSAI_API_KEY = $accessToken # activate in session
    }

    return $accessToken
}