functions/helpers.ps1

#these are private helper functions

Function _newFacetLink {
    # https://docs.bsky.app/docs/advanced-guides/post-richtext
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, HelpMessage = 'The Bluesky message with the links')]
        [string]$Message,
        [Parameter(Mandatory, HelpMessage = 'The text of the link')]
        [string]$Text,
        [Parameter(HelpMessage = 'The URI of the link')]
        [string]$Uri,
        [Parameter(HelpMessage = 'The DID of the mention')]
        [string]$DiD,
        [Parameter(HelpMessage = 'The Tag text')]
        [string]$Tag,
        [ValidateSet('link', 'mention', 'tag')]
        [string]$FacetType = 'link'
    )


    $PSDefaultParameterValues['_verbose:block'] = 'PRIVATE'
    _verbose -Message ($strings.NewFacet -f $FacetType, $Text, $Message)
    $feature = Switch ($FacetType) {
        'link' {
            _verbose -Message ($strings.FacetLink -f $Uri)
            [PSCustomObject]@{
                '$type' = 'app.bsky.richtext.facet#link'
                uri     = $Uri
            }
        }
        'mention' {
            _verbose -Message ($strings.FacetMention -f $DID)
            [PSCustomObject]@{
                '$type' = 'app.bsky.richtext.facet#mention'
                did     = $DiD
            }
        }
        'tag' {
            _verbose -Message ($strings.FacetTag -f $Tag)
            [PSCustomObject]@{
                '$type' = 'app.bsky.richtext.facet#tag'
                tag     = $Tag
            }
        }
    }

    if ($text -match '\[|\]|\(\)') {
        _verbose -Message $strings.RxEscape
        $text = [regex]::Escape($text)
    }
    #the comparison test is case-sensitive
    if (([regex]$Text).IsMatch($Message)) {
        #properties of the facet object are also case-sensitive
        $m = ([regex]$Text).match($Message)
        [PSCustomObject]@{
            index    = [ordered]@{
                byteStart = $m.index
                byteEnd   = ($m.value.length) + ($m.index)
            }
            features = @(
                $feature
            )
        }
    }
    else {
        Write-Warning ($strings.TextNotFound -f $Text, $Message)
    }
}

Function _convertDidToAt {
    [cmdletbinding()]
    Param(
        [parameter(Mandatory, HelpMessage = 'The DID to convert')]
        [ValidatePattern('^did:plc:')]
        [string]$did
    )

    #did:plc:qrllvid7s54k4hnwtqxwetrf
    $at = $did.replace('did:', 'at://')
    $at
}

Function _convertAT {
    [cmdletbinding()]
    Param(
        [parameter(Mandatory, HelpMessage = 'The AT string to convert')]
        [ValidatePattern('^at://')]
        [string]$at
    )

    #at://did:plc:qrllvid7s54k4hnwtqxwetrf/app.bsky.feed.post/3l7e5jvorof2t
    $split = $at -split '/' | where { $_ -match '\w' }
    #this part might need to change in the future depending on the type of link
    $publicUri = 'https://bsky.app/profile/'
    $publicUri += '{0}/post/{1}' -f $split[1], $split[-1]
    $publicUri
}
function _getPostText {
    [cmdletbinding()]
    param (
        [string]$AT,
        [hashtable]$Headers
    )
    if ($AT) {
        $apiUrl = "$PDSHost/xrpc/app.bsky.feed.getPosts?uris=$at"
        $r = Invoke-RestMethod -Uri $apiUrl -Method Get -Headers $headers
        _newLogData -apiUrl $apiUrl -command $MyInvocation.MyCommand | _updateLog
        $r.posts.record.text
    }
}

Function _newSessionObject {
    <#
Convert the API response into a structured and type object
 
did : did:plc:ohgsqpfsbocaaxusxqlgfvd7
didDoc : @{@context=System.Object[]; id=did:plc:ohgsqpfsbocaaxusxqlgfvd7; alsoKnownAs=System.Object[];
                  verificationMethod=System.Object[]; service=System.Object[]}
handle : jdhitsolutions.com
email : jhicks@jdhitsolutions.com
emailConfirmed : True
emailAuthFactor : False
accessJwt : eyJ0eXAiOiJhdCtqd3QiLCJhbGciOiJFUzI1NksifQ.eyJzY29wZSI...
refreshJwt : eyJ0eXAiOiJyZWZyZXNoK2p3dCIsImFsZyI6IkVTMjU2SyJ9.eyJz...
active : True
Date : 10/29/2024 9:56:40 AM
Age : 01:09:58.6486840
#>


    [CmdletBinding()]
    Param(
        [Parameter(
            Position = 0,
            Mandatory,
            HelpMessage = 'The Bluesky session object',
            ValueFromPipeline
        )]
        [object]$InputObject
    )
    Begin {
        #not used
    }
    Process {
        [PSCustomObject]@{
            PSTypeName     = 'PSBlueskySession'
            Handle         = $InputObject.handle
            Email          = $InputObject.email
            Active         = $InputObject.active
            AccessJwt      = $InputObject.accessJwt
            RefreshJwt     = $InputObject.refreshJwt
            DiD            = $InputObject.did
            DidDoc         = $InputObject.didDoc
            Date           = $InputObject.Date
            LoggingEnabled = $InputObject.LoggingEnabled
            LogFile        = $InputObject.LogFile
        }
    } #process
    End {
        #11 Nov 2024 Move these definitions to the root module
        <#
        Update-TypeData -TypeName 'PSBlueskySession' -MemberType AliasProperty -MemberName UserName -Value handle -Force
        Update-TypeData -TypeName 'PSBlueskySession' -MemberType AliasProperty -MemberName AccessToken -Value AccessJwt -Force
        Update-TypeData -TypeName 'PSBlueskySession' -MemberType AliasProperty -MemberName RefreshToken -Value RefreshJwt -Force
        Update-TypeData -TypeName 'PSBlueskySession' -MemberType ScriptProperty -MemberName Age -Value { (Get-Date) - $this.Date } -Force
        Update-TypeData -TypeName 'PSBlueskySession' -MemberType ScriptMethod -MemberName Refresh -Value {Update-BskySession -RefreshToken $this.RefreshJwt} -Force
        #>

    }
}

Function _CreateSession {
    #there is an API limit of 300 per day for this endpoint
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, HelpMessage = 'A PSCredential with your Bluesky username and password')]
        [PSCredential]$Credential
    )

    $PSDefaultParameterValues['_verbose:block'] = 'PRIVATE'
    #Create a logon session
    $headers = @{
        'Content-Type' = 'application/json'
    }

    $apiUrl = "$PDSHOST/xrpc/com.atproto.server.createSession"
    $body = @{
        identifier = $Credential.UserName
        password   = $Credential.GetNetworkCredential().Password
    } | ConvertTo-Json

    _verbose -Message ($strings.NewSession -f $credential.UserName)

    $splat = @{
        Uri         = $apiUrl
        Method      = 'Post'
        Headers     = $headers
        Body        = $Body
        ErrorAction = 'Stop'
    }
    Try {
        # 11 Nov 2024 -create a synchronized hashtable and use a background runspace
        # to update the session every 15 minutes

        # 16 Jan 2025 - increase the session refresh time to 60 minutes to minimize
        # the number of API calls

        $r = Invoke-RestMethod @splat
        _newLogData -apiUrl $apiUrl -command $MyInvocation.MyCommand | _updateLog
        _verbose -Message $strings.DefineSyncHash
        <# $script:BskySession = [hashtable]::Synchronized(@{
                Handle = $r.handle
                Email = $r.email
                Active = $r.active
                AccessJwt = $r.accessJwt
                RefreshJwt = $r.refreshJwt
                DiD = $r.did
                DidDoc = $r.didDoc
                Date = Get-Date
                LoggingEnabled = $global:bskyLoggingEnabled
                LogFile = $global:bskyLogFile
            }) #>

        $script:BskySession.Handle = $r.handle
        $script:BskySession.Email = $r.email
        $script:BskySession.Active = $r.active
        $script:BskySession.AccessJwt = $r.accessJwt
        $script:BskySession.RefreshJwt = $r.refreshJwt
        $script:BskySession.DiD = $r.did
        $script:BskySession.DidDoc = $r.didDoc
        $script:BskySession.Date = Get-Date

        # DELETE?
        #script:accessJwt = $script:BskySession.accessJwt
        #script:refreshJwt = $script:BskySession.refreshJwt

        $script:BskySession | _newSessionObject

        #create a runspace to update the session every 60 minutes
        _verbose -message $strings.NewRunspace
        $newRunspace = [RunspaceFactory]::CreateRunspace()
        $newRunspace.ApartmentState = 'STA'
        $newRunspace.ThreadOptions = 'ReuseThread'
        $newRunspace.Open()
        $newRunspace.SessionStateProxy.SetVariable('BskySession', $script:BskySession)

        $script:PSCmd = [PowerShell]::Create().AddScript({
                $PDSHOST = 'https://bsky.social'
                $i = 0
                Function _newLogData {
                    [CmdletBinding()]
                    Param(
                        [Parameter(ValueFromPipeline, Mandatory)]
                        [string]$apiUrl,
                        [Parameter(Mandatory)]
                        [string]$Command
                    )

                    [regex]$rx = '((app)|(com)|(chat))(\.\w+){3}'
                    $ep = $rx.match($apiUrl).value
                    [PSCustomObject]@{
                        Date     = Get-Date
                        Uri      = $apiUrl
                        Endpoint = $rx.match($apiUrl).value
                        Name     = $ep.split('.')[-1]
                        Command  = $command
                    }
                }

                Function _updateLog {
                    [CmdletBinding()]
                    Param(
                        [Parameter(ValueFromPipeline, Mandatory)]
                        [object]$InputObject,
                        [string]$Path = $BskySession.LogFile
                    )
                    Begin {
                        if ($BskySession.LoggingEnabled) {
                            $existing = [System.Collections.Generic.List[object]]::new()
                            #import existing data if found
                            if (Test-Path $Path) {
                                $existing.AddRange([object[]](Get-Content -Path $Path -Raw | ConvertFrom-Json))
                            }
                        } #if Logging
                    }
                    Process {
                        if ($BskySession.LoggingEnabled) {
                            $existing.Add($InputObject)
                        }
                    }
                    End {
                        If ($BskySession.LoggingEnabled) {
                            $existing | ConvertTo-Json | Out-File -FilePath $Path -Force -Encoding utf8
                        }
                    }
                }
                #!!!!!!! MY DEBUG CODE
                # $debugFile = "d:\temp\debug.log"
                Do {
                    $ts = New-TimeSpan -Start $BskySession.Date -End (Get-Date)
                    if ($ts.TotalMinutes -ge 60) {
                        $i++
                        #refresh
                        $headers = @{
                            Authorization  = "Bearer $($BskySession.refreshJwt)"
                            'Content-Type' = 'application/json'
                        }
                        $apiUrl = "$PDSHost/xrpc/com.atproto.server.refreshSession"
                        Try {
                            $splat = @{
                                Uri           = $apiUrl
                                Method        = 'Post'
                                Headers       = $headers
                                ErrorAction   = 'Stop'
                                ErrorVariable = 'e'
                            }
                            $r = Invoke-RestMethod @splat
                            _newLogData -apiUrl $apiUrl -command RunspaceRefreshSession | _updateLog
                            #!!!!!!! DEBUG CODE
                            # "[$((Get-Date).TimeOfDay)] Refreshing session" | Out-File -FilePath $debugFile -Append
                            # $r | Out-File -FilePath $debugFile -Append
                            #!!!!!!! DEBUG CODE
                            $BskySession.accessJwt = $r.accessJwt
                            $BskySession.refreshJwt = $r.refreshJwt
                            $BskySession.Active = $r.active
                            $BskySession.Date = Get-Date
                            $BskySession['RunspaceOperation'] = $i
                        } #try
                        Catch {
                            #!!!!!!! DEBUG CODE
                            #"[$(Get-Date)] Failed to authenticate or refresh the session. $($_.Exception.Message)" | out-file $debugFile -Append
                            #$e.message | out-file $debugFile -Append
                        } #catch
                    }
                    Start-Sleep -Seconds 60
                } While ($True)
                Exit
            })

        $script:PSCmd.runspace = $newRunspace
        _verbose -Message $strings.StartRunspace
        [Void]($psCmd.BeginInvoke())

    } #try
    Catch {
        throw $_
    }
    if ($null -eq $script:BskySession.accessjwt) {
        Write-Warning $strings.FailAuthenticate
    }
}

#a private function to display customized verbose messages
function _verbose {
    [CmdletBinding()]
    param(
        [Parameter(Position = 0)]
        [string]$Message,
        [string]$Block = 'PROCESS',
        [string]$Command
    )

    #Display each command name in a different color sequence
    if ($bskyPreferences.Contains($Command)) {
        [string]$ANSI = $bskyPreferences[$Command]
    }
    else {
        [string]$ANSI = $bskyPreferences['DefaultCommand']
    }

    $BlockString = $Block.ToUpper().PadRight(7, ' ')
    $Reset = "$([char]27)[0m"
    $ToD = (Get-Date).TimeOfDay
    $AnsiCommand = "$ANSI$($command)"
    $Italic = "$([char]27)[3m"
    if ($Host.Name -eq 'Windows PowerShell ISE Host') {
        #this code should never run in this module since it requires PowerShell 7
        $msg = '[{0:hh\:mm\:ss\:ffff} {1}] {2}-> {3}' -f $Tod, $BlockString, $Command, $Message
    }
    else {
        $msg = '[{0:hh\:mm\:ss\:ffff} {1}] {2}{3}-> {4} {5}{3}' -f $Tod, $BlockString, $AnsiCommand, $Reset, $Italic, $Message
    }
    #use the built-in Write-Verbose cmdlet
    Microsoft.PowerShell.Utility\Write-Verbose -Message $msg

}

#this is a test and trouble shooting function
Function __GetBskyVar {
    [cmdletbinding()]
    [OutputType('None')]
    Param()

    '**Script scope**'
    gv bsky* -Scope script

    "`n**Global scope**"
    gv bsky* -Scope global
}
######
# 4 Nov 2024 -moved this to a public function, Update-BskySession
<#
Function X_RefreshSession {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory, HelpMessage = 'The refresh token')]
        [string]$RefreshToken
        )
 
        #Refresh a Bluesky session
        Write-Verbose "[$((Get-Date).TimeOfDay)] Refreshing a Bluesky logon session for $($script:BskySession.handle)"
        $headers = @{
            Authorization = "Bearer $RefreshToken"
            'Content-Type' = 'application/json'
        }
        $apiUrl = "$PDSHost/xrpc/com.atproto.server.refreshSession"
        Try {
            $script:BskySession = Invoke-RestMethod -Uri $apiUrl -Method Post -Headers $headers -ErrorAction Stop | _newSessionObject
 
            $script:accessJwt = $script:BskySession.accessJwt
            $script:refreshJwt = $script:BskySession.refreshJwt
            #return the session
            $script:BskySession
        } #try
        Catch {
            Write-Warning "Failed to authenticate or refresh the session. $($_.Exception.Message)"
        }
    }
 
#>