Types/OpenPackage.Source/AtProtocol.ps1

<#
.SYNOPSIS
    Gets at protocol data
.DESCRIPTION
    Gets data from the at protocol.
.EXAMPLE
    Get-OpenPackage at://mrpowershell.com/app.bsky.actor.profile
#>

param(
[Parameter(Mandatory)]
[string[]]
$AtUri,

# A list of file wildcards to include.
[Parameter(ValueFromPipelineByPropertyName)]
[SupportsWildcards()]
[string[]]
$Include,

# A list of file wildcards to exclude.
[Parameter(ValueFromPipelineByPropertyName)]
[SupportsWildcards()]
[string[]]
$Exclude,

# The base path within the package.
# Content should be added beneath this base path.
[string]
$BasePath = '/',

# A content type map.
# This maps extensions and URIs to a content type.
[Collections.IDictionary]
$TypeMap = $(
    ([PSCustomObject]@{PSTypeName='OpenPackage.ContentTypeMap'}).TypeMap
),

# The compression option.
[IO.Packaging.CompressionOption]
[Alias('CompressionLevel')]
$CompressionOption = 'Superfast',

# The personal data server. This is used in At Protocol requests.
[string]
$pds = "https://bsky.social",

# The current package
[IO.Packaging.Package]
$Package,

# The batch size
[ValidateRange(1,100)]
[int]
$BatchSize = 100,

# Any additional headers to pass into a web request.
[Alias('Header')]
[Collections.IDictionary]
$Headers,

# The number of records to get
[long]
$First,

# If number of records to skip
[long]
$Skip
)


if (-not $package) {
    $memoryStream = [IO.MemoryStream]::new()
    $package = [IO.Packaging.Package]::Open($memoryStream, 'OpenOrCreate', 'ReadWrite')
    Add-Member -InputObject $package NoteProperty MemoryStream $memoryStream -Force
}

if (-not $this) {$this = $package}

$sources = [PSCustomObject]@{PSTypeName='OpenPackage.Source'}

filter packAtProtoRecord {
    # Declare a package uri for the segment.
    # (make sure to switch did colons to something else, so that the files can unpack cleanly regardless of OS)
    $currentPackageUri = "/$($atMatch.did -replace ':','_')/$($matches.type)/$($matches.rkey).json"

    # If the part exists,
    if ($Package.PartExists($currentPackageUri)) {
        $Package.DeletePart($currentPackageUri) # recreate it.
        if (-not $?) { return }
    }
    $atPart = $Package.CreatePart($currentPackageUri, 'application/json', $CompressionOption)
    if (-not $atPart) { continue }  
    # Get the stream.
    $atStream = $atPart.GetStream()
    if (-not $atStream) { continue }
    # Turn our message into json, and get the bytes.
    $atJsonBytes = $outputEncoding.GetBytes(
        ($atRecord | ConvertTo-Json -Depth 100)
    )

    # Then write them to the stream,
    $atStream.Write($atJsonBytes, 0, $atJsonBytes.Count)

    # clean up,
    $atStream.Close()
    $atStream.Dispose()        
}

foreach ($at in $AtUri) {
    # and declare a pattern to pick apart an at uri
    $atPattern = 'at://(?<did>[^/]+)/(?<type>[^/]+)(?:/(?<rkey>.+?$))?'

    # If this does not match the pattern or we don't have a type, we are done.
    if ($at -notmatch $atPattern -or 
        -not $matches.type) { return }
    
    # Store our match information before anything else needs to `-match`.
    $atMatch = [Ordered]@{} + $Matches
    # Create a package in memory
    
    if (-not $package.PackageProperties.Identifier) {
        $package.PackageProperties.Identifier = $atMatch.did
    }            

    # If we have a type and rkey, we are after a single record.
    if ($atMatch.type -and $atMatch.rkey) {
        $atRecord = $sources.AtRecord($atMatch.did, $atMatch.type, $atMatch.rkey, $pds)
        if (-not $atRecord) { continue }
        packAtProtoRecord
    }
    elseif ($atMatch.type -match '\.') {
        # If there are dots in the type, it is a collection
        foreach ($atRecord in $sources.AtType(
            $atMatch.did, $atMatch.type, $BatchSize, $First, $Skip, $pds
        )) {
             # If the uri is not an at uri
            if ($atRecord.uri -notmatch $atPattern) {
                # continue to the next record
                continue
            }
            packAtProtoRecord
        }
    } 
    else 
    {
        try {
            $atBlob = $sources.AtBlob($matches.did, $matches.type)
            $atContentType = $atBlob.Headers.'Content-Type'
            if (-not $atContentType) {
                continue                
            }
            $currentPackageUri = "/$($matches.did -replace ':','_')/$($matches.type).$(@($atContentType -split '[/\+]')[-1])"
            $blobPart = $Package.CreatePart($currentPackageUri, $atContentType, $CompressionOption)
            $blobStream = $blobPart.GetStream() 
            $memoryStream = 
                if ($atBlob.Content -is [byte[]]) {
                    [IO.MemoryStream]::new($atBlob.Content)
                } else {
                    [IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes($atBlob.Content))
                }
            $memoryStream.CopyTo($blobStream)
            $memoryStream.Close()
            $null = $memoryStream.DisposeAsync()

            $blobStream.Close()
            $null = $blobStream.DisposeAsync()

        } catch {
            Write-Debug "Unable to get $at : $_"
            continue
        }
    }       
}

# Only return a package if it is not empty.
if (@($package.GetParts()).Length) {
    return $Package
}