diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..36bd853
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [StartAutomating]
diff --git a/Build/GitHub/Actions/WebSocketAction.ps1 b/Build/GitHub/Actions/WebSocketAction.ps1
new file mode 100644
index 0000000..275bbbe
--- /dev/null
+++ b/Build/GitHub/Actions/WebSocketAction.ps1
@@ -0,0 +1,349 @@
+<#
+.Synopsis
+ GitHub Action for WebSocket
+.Description
+ GitHub Action for WebSocket. This will:
+
+ * Import WebSocket
+ * If `-Run` is provided, run that script
+ * Otherwise, unless `-SkipScriptFile` is passed, run all *.WebSocket.ps1 files beneath the workflow directory
+ * If any `-ActionScript` was provided, run scripts from the action path that match a wildcard pattern.
+
+ If you will be making changes using the GitHubAPI, you should provide a -GitHubToken
+ If none is provided, and ENV:GITHUB_TOKEN is set, this will be used instead.
+ Any files changed can be outputted by the script, and those changes can be checked back into the repo.
+ Make sure to use the "persistCredentials" option with checkout.
+#>
+
+param(
+# A PowerShell Script that uses WebSocket.
+# Any files outputted from the script will be added to the repository.
+# If those files have a .Message attached to them, they will be committed with that message.
+[string]
+$Run,
+
+# If set, will not process any files named *.WebSocket.ps1
+[switch]
+$SkipScriptFile,
+
+# A list of modules to be installed from the PowerShell gallery before scripts run.
+[string[]]
+$InstallModule,
+
+# If provided, will commit any remaining changes made to the workspace with this commit message.
+[string]
+$CommitMessage,
+
+# If provided, will checkout a new branch before making the changes.
+# If not provided, will use the current branch.
+[string]
+$TargetBranch,
+
+# The name of one or more scripts to run, from this action's path.
+[string[]]
+$ActionScript,
+
+# The github token to use for requests.
+[string]
+$GitHubToken = '{{ secrets.GITHUB_TOKEN }}',
+
+# The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com.
+[string]
+$UserEmail,
+
+# The user name associated with a git commit.
+[string]
+$UserName,
+
+# If set, will not push any changes made to the repository.
+# (they will still be committed unless `-NoCommit` is passed)
+[switch]
+$NoPush,
+
+# If set, will not commit any changes made to the repository.
+# (this also implies `-NoPush`)
+[switch]
+$NoCommit
+)
+
+$ErrorActionPreference = 'continue'
+"::group::Parameters" | Out-Host
+[PSCustomObject]$PSBoundParameters | Format-List | Out-Host
+"::endgroup::" | Out-Host
+
+$gitHubEventJson = [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH)
+$gitHubEvent =
+ if ($env:GITHUB_EVENT_PATH) {
+ $gitHubEventJson | ConvertFrom-Json
+ } else { $null }
+"::group::Parameters" | Out-Host
+$gitHubEvent | Format-List | Out-Host
+"::endgroup::" | Out-Host
+
+
+$anyFilesChanged = $false
+$ActionModuleName = 'PSJekyll'
+$actorInfo = $null
+
+
+$checkDetached = git symbolic-ref -q HEAD
+if ($LASTEXITCODE) {
+ "::warning::On detached head, skipping action" | Out-Host
+ exit 0
+}
+
+function InstallActionModule {
+ param([string]$ModuleToInstall)
+ $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File |
+ Where-Object Name -eq "$($moduleToInstall).psd1" |
+ Where-Object {
+ $(Get-Content $_.FullName -Raw) -match 'ModuleVersion'
+ }
+ if (-not $moduleInWorkspace) {
+ $availableModules = Get-Module -ListAvailable
+ if ($availableModules.Name -notcontains $moduleToInstall) {
+ Install-Module $moduleToInstall -Scope CurrentUser -Force -AcceptLicense -AllowClobber
+ }
+ Import-Module $moduleToInstall -Force -PassThru | Out-Host
+ } else {
+ Import-Module $moduleInWorkspace.FullName -Force -PassThru | Out-Host
+ }
+}
+function ImportActionModule {
+ #region -InstallModule
+ if ($InstallModule) {
+ "::group::Installing Modules" | Out-Host
+ foreach ($moduleToInstall in $InstallModule) {
+ InstallActionModule -ModuleToInstall $moduleToInstall
+ }
+ "::endgroup::" | Out-Host
+ }
+ #endregion -InstallModule
+
+ if ($env:GITHUB_ACTION_PATH) {
+ $LocalModulePath = Join-Path $env:GITHUB_ACTION_PATH "$ActionModuleName.psd1"
+ if (Test-path $LocalModulePath) {
+ Import-Module $LocalModulePath -Force -PassThru | Out-String
+ } else {
+ throw "Module '$ActionModuleName' not found"
+ }
+ } elseif (-not (Get-Module $ActionModuleName)) {
+ throw "Module '$ActionModuleName' not found"
+ }
+
+ "::notice title=ModuleLoaded::$ActionModuleName Loaded from Path - $($LocalModulePath)" | Out-Host
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "# $($ActionModuleName)" |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+}
+function InitializeAction {
+ #region Custom
+ #endregion Custom
+
+ # Configure git based on the $env:GITHUB_ACTOR
+ if (-not $UserName) { $UserName = $env:GITHUB_ACTOR }
+ if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID }
+ $actorInfo =
+ if ($GitHubToken -notmatch '^\{{2}' -and $GitHubToken -notmatch '\}{2}$') {
+ Invoke-RestMethod -Uri "https://api.github.com/user/$actorID" -Headers @{ Authorization = "token $GitHubToken" }
+ } else {
+ Invoke-RestMethod -Uri "https://api.github.com/user/$actorID"
+ }
+
+ if (-not $UserEmail) { $UserEmail = "$UserName@noreply.github.com" }
+ git config --global user.email $UserEmail
+ git config --global user.name $actorInfo.name
+
+ # Pull down any changes
+ git pull | Out-Host
+
+ if ($TargetBranch) {
+ "::notice title=Expanding target branch string $targetBranch" | Out-Host
+ $TargetBranch = $ExecutionContext.SessionState.InvokeCommand.ExpandString($TargetBranch)
+ "::notice title=Checking out target branch::$targetBranch" | Out-Host
+ git checkout -b $TargetBranch | Out-Host
+ git pull | Out-Host
+ }
+}
+
+function InvokeActionModule {
+ $myScriptStart = [DateTime]::Now
+ $myScript = $ExecutionContext.SessionState.PSVariable.Get("Run").Value
+ if ($myScript) {
+ Invoke-Expression -Command $myScript |
+ . ProcessOutput |
+ Out-Host
+ return
+ }
+ $myScriptTook = [Datetime]::Now - $myScriptStart
+ $MyScriptFilesStart = [DateTime]::Now
+
+ $myScriptList = @()
+ $shouldSkip = $ExecutionContext.SessionState.PSVariable.Get("SkipScriptFile").Value
+ if ($shouldSkip) {
+ return
+ }
+ $scriptFiles = @(
+ Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE |
+ Where-Object Name -Match "\.$($ActionModuleName)\.ps1$"
+ if ($ActionScript) {
+ if ($ActionScript -match '^\s{0,}/' -and $ActionScript -match '/\s{0,}$') {
+ $ActionScriptPattern = $ActionScript.Trim('/').Trim() -as [regex]
+ if ($ActionScriptPattern) {
+ $ActionScriptPattern = [regex]::new($ActionScript.Trim('/').Trim(), 'IgnoreCase,IgnorePatternWhitespace', [timespan]::FromSeconds(0.5))
+ Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH |
+ Where-Object { $_.Name -Match "\.$($ActionModuleName)\.ps1$" -and $_.FullName -match $ActionScriptPattern }
+ }
+ } else {
+ Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH |
+ Where-Object Name -Match "\.$($ActionModuleName)\.ps1$" |
+ Where-Object FullName -Like $ActionScript
+ }
+ }
+ ) | Select-Object -Unique
+ $scriptFiles |
+ ForEach-Object -Begin {
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "## $ActionModuleName Scripts" |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ } -Process {
+ $myScriptList += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/')
+ $myScriptCount++
+ $scriptFile = $_
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "### $($scriptFile.Fullname -replace [Regex]::Escape($env:GITHUB_WORKSPACE))" |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ $scriptCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($scriptFile.FullName, 'ExternalScript')
+ foreach ($requiredModule in $CommandInfo.ScriptBlock.Ast.ScriptRequirements.RequiredModules) {
+ if ($requiredModule.Name -and
+ (-not $requiredModule.MaximumVersion) -and
+ (-not $requiredModule.RequiredVersion)
+ ) {
+ InstallActionModule $requiredModule.Name
+ }
+ }
+ $scriptFileOutputs = . $scriptCmd
+ $scriptFileOutputs |
+ . ProcessOutput |
+ Out-Host
+ }
+
+ $MyScriptFilesTook = [Datetime]::Now - $MyScriptFilesStart
+ $SummaryOfMyScripts = "$myScriptCount $ActionModuleName scripts took $($MyScriptFilesTook.TotalSeconds) seconds"
+ $SummaryOfMyScripts |
+ Out-Host
+ if ($env:GITHUB_STEP_SUMMARY) {
+ $SummaryOfMyScripts |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ #region Custom
+ #endregion Custom
+}
+
+function OutError {
+ $anyRuntimeExceptions = $false
+ foreach ($err in $error) {
+ $errParts = @(
+ "::error "
+ @(
+ if ($err.InvocationInfo.ScriptName) {
+ "file=$($err.InvocationInfo.ScriptName)"
+ }
+ if ($err.InvocationInfo.ScriptLineNumber -ge 1) {
+ "line=$($err.InvocationInfo.ScriptLineNumber)"
+ if ($err.InvocationInfo.OffsetInLine -ge 1) {
+ "col=$($err.InvocationInfo.OffsetInLine)"
+ }
+ }
+ if ($err.CategoryInfo.Activity) {
+ "title=$($err.CategoryInfo.Activity)"
+ }
+ ) -join ','
+ "::"
+ $err.Exception.Message
+ if ($err.CategoryInfo.Category -eq 'OperationStopped' -and
+ $err.CategoryInfo.Reason -eq 'RuntimeException') {
+ $anyRuntimeExceptions = $true
+ }
+ ) -join ''
+ $errParts | Out-Host
+ if ($anyRuntimeExceptions) {
+ exit 1
+ }
+ }
+}
+
+function PushActionOutput {
+ if ($anyFilesChanged) {
+ "::notice::$($anyFilesChanged) Files Changed" | Out-Host
+ }
+ if ($CommitMessage -or $anyFilesChanged) {
+ if ($CommitMessage) {
+ Get-ChildItem $env:GITHUB_WORKSPACE -Recurse |
+ ForEach-Object {
+ $gitStatusOutput = git status $_.Fullname -s
+ if ($gitStatusOutput) {
+ git add $_.Fullname
+ }
+ }
+
+ git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage)
+ }
+
+ $checkDetached = git symbolic-ref -q HEAD
+ if (-not $LASTEXITCODE -and -not $NoPush -and -not $noCommit) {
+ if ($TargetBranch -and $anyFilesChanged) {
+ "::notice::Pushing Changes to $targetBranch" | Out-Host
+ git push --set-upstream origin $TargetBranch
+ } elseif ($anyFilesChanged) {
+ "::notice::Pushing Changes" | Out-Host
+ git push
+ }
+ "Git Push Output: $($gitPushed | Out-String)"
+ } else {
+ "::notice::Not pushing changes (on detached head)" | Out-Host
+ $LASTEXITCODE = 0
+ exit 0
+ }
+ }
+}
+
+filter ProcessOutput {
+ $out = $_
+ $outItem = Get-Item -Path $out -ErrorAction Ignore
+ if (-not $outItem -and $out -is [string]) {
+ $out | Out-Host
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "> $out" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ return
+ }
+ $fullName, $shouldCommit =
+ if ($out -is [IO.FileInfo]) {
+ $out.FullName, (git status $out.Fullname -s)
+ } elseif ($outItem) {
+ $outItem.FullName, (git status $outItem.Fullname -s)
+ }
+ if ($shouldCommit -and -not $NoCommit) {
+ "$fullName has changed, and should be committed" | Out-Host
+ git add $fullName
+ if ($out.Message) {
+ git commit -m "$($out.Message)" | Out-Host
+ } elseif ($out.CommitMessage) {
+ git commit -m "$($out.CommitMessage)" | Out-Host
+ } elseif ($gitHubEvent.head_commit.message) {
+ git commit -m "$($gitHubEvent.head_commit.message)" | Out-Host
+ }
+ $anyFilesChanged = $true
+ }
+ $out
+}
+
+. ImportActionModule
+. InitializeAction
+. InvokeActionModule
+. PushActionOutput
+. OutError
\ No newline at end of file
diff --git a/Build/WebSocket.GitHubAction.PSDevOps.ps1 b/Build/WebSocket.GitHubAction.PSDevOps.ps1
new file mode 100644
index 0000000..5813b54
--- /dev/null
+++ b/Build/WebSocket.GitHubAction.PSDevOps.ps1
@@ -0,0 +1,10 @@
+#requires -Module PSDevOps
+Import-BuildStep -SourcePath (
+ Join-Path $PSScriptRoot 'GitHub'
+) -BuildSystem GitHubAction
+
+$PSScriptRoot | Split-Path | Push-Location
+
+New-GitHubAction -Name "UseWebSocket" -Description 'Work with WebSockets in PowerShell' -Action WebSocketAction -Icon chevron-right -OutputPath .\action.yml
+
+Pop-Location
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index abb0512..6f73da7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,26 @@
-## WebSocket 0.1
-
> Like It? [Star It](https://github.com/PowerShellWeb/WebSocket)
> Love It? [Support It](https://github.com/sponsors/StartAutomating)
+## WebSocket 0.1.1
+
+* WebSocket GitHub Action
+ * Run any `*.WebSocket.ps1` files in a repository (#24)
+* WebSocket container updates
+ * Container now runs mounted `*.WebSocket.ps1` files (#26)
+* Get-WebSocket improvements:
+ * New Parameters:
+ * -Maximum (#22)
+ * -TimeOut (#23)
+ * -WatchFor (#29)
+ * -RawText (#30)
+ * -Binary (#31)
+* WebSocket Testing (#25)
+* Adding FUNDING.yml (#14)
+
+---
+
+## WebSocket 0.1
+
* Initial Release of WebSocket module
* Get-WebSocket gets content from a WebSocket
* Docker container for WebSocket
diff --git a/Commands/Get-WebSocket.ps1 b/Commands/Get-WebSocket.ps1
index 3723f81..45e28eb 100644
--- a/Commands/Get-WebSocket.ps1
+++ b/Commands/Get-WebSocket.ps1
@@ -3,14 +3,35 @@ function Get-WebSocket {
.SYNOPSIS
WebSockets in PowerShell.
.DESCRIPTION
- Get-WebSocket allows you to connect to a websocket and handle the output.
+ Get-WebSocket gets a websocket.
+
+ This will create a job that connects to a WebSocket and outputs the results.
+
+ If the `-Watch` parameter is provided, will output a continous stream of objects.
.EXAMPLE
# Create a WebSocket job that connects to a WebSocket and outputs the results.
- Get-WebSocket -WebSocketUri "wss://localhost:9669"
+ Get-WebSocket -WebSocketUri "wss://localhost:9669/"
.EXAMPLE
# Get is the default verb, so we can just say WebSocket.
+ # `-Watch` will output a continous stream of objects from the websocket.
+ # For example, let's Watch BlueSky, but just the text.
+ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ % {
+ $_.commit.record.text
+ }
+ .EXAMPLE
+ # Watch BlueSky, but just the text and spacing
+ $blueSkySocketUrl = "wss://jetstream2.us-$(
+ 'east','west'|Get-Random
+ ).bsky.network/subscribe?$(@(
+ "wantedCollections=app.bsky.feed.post"
+ ) -join '&')"
+ websocket $blueSkySocketUrl -Watch |
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+ .EXAMPLE
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
.EXAMPLE
+ # Watch BlueSky, but just the emoji
websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
Foreach-Object {
$in = $_
@@ -20,15 +41,53 @@ function Get-WebSocket {
}
.EXAMPLE
$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
- websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
Foreach-Object {
$in = $_
+ $spacing = (' ' * (Get-Random -Minimum 0 -Maximum 7))
if ($in.commit.record.text -match "(?>(?:$emojiPattern|\#\w+)") {
- Write-Host $matches.0 -NoNewline
+ $match = $matches.0
+ Write-Host $spacing,$match,$spacing -NoNewline
}
}
+ .EXAMPLE
+ websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ Where-Object {
+ $_.commit.record.embed.'$type' -eq 'app.bsky.embed.external'
+ } |
+ Foreach-Object {
+ $_.commit.record.embed.external.uri
+ }
+ .EXAMPLE
+ # BlueSky, but just the hashtags
+ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+ }
+ .EXAMPLE
+ # BlueSky, but just the hashtags (as links)
+ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ if ($psStyle.FormatHyperlink) {
+ $psStyle.FormatHyperlink($matches.0, "https://bsky.app/search?q=$([Web.HttpUtility]::UrlEncode($matches.0))")
+ } else {
+ $matches.0
+ }
+ }
+ }
+ .EXAMPLE
+ websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$args.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+ {$args.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+'}={
+ $matches.0
+ }
+ }
#>
[CmdletBinding(PositionalBinding=$false)]
+ [Alias('WebSocket')]
param(
# The Uri of the WebSocket to connect to.
[Parameter(Position=0,ValueFromPipelineByPropertyName)]
@@ -40,6 +99,7 @@ function Get-WebSocket {
$Handler,
# Any variables to declare in the WebSocket job.
+ # These variables will also be added to the job as properties.
[Collections.IDictionary]
$Variable = @{},
@@ -77,6 +137,46 @@ function Get-WebSocket {
[switch]
$Watch,
+ # If set, will output the raw text that comes out of the WebSocket.
+ [Alias('Raw')]
+ [switch]
+ $RawText,
+
+ # If set, will output the raw bytes that come out of the WebSocket.
+ [Alias('RawByte','RawBytes','Bytes','Byte')]
+ [switch]
+ $Binary,
+
+ # If set, will watch the output of a WebSocket job for one or more conditions.
+ # The conditions are the keys of the dictionary, and can be a regex, a string, or a scriptblock.
+ # The values of the dictionary are what will happen when a match is found.
+ [ValidateScript({
+ $keys = $_.Keys
+ $values = $_.values
+ foreach ($key in $keys) {
+ if ($key -isnot [scriptblock]) {
+ throw "Keys '$key' must be a scriptblock"
+ }
+ }
+ foreach ($value in $values) {
+ if ($value -isnot [scriptblock] -and $value -isnot [string]) {
+ throw "Value '$value' must be a string or scriptblock"
+ }
+ }
+ return $true
+ })]
+ [Alias('WhereFor','Wherefore')]
+ [Collections.IDictionary]
+ $WatchFor,
+
+ # The timeout for the WebSocket connection. If this is provided, after the timeout elapsed, the WebSocket will be closed.
+ [TimeSpan]
+ $TimeOut,
+
+ # The maximum number of messages to receive before closing the WebSocket.
+ [long]
+ $Maximum,
+
# The maximum time to wait for a connection to be established.
# By default, this is 7 seconds.
[TimeSpan]
@@ -123,18 +223,39 @@ function Get-WebSocket {
$ws = $WebSocket
}
+ $webSocketStartTime = $Variable.WebSocketStartTime = [DateTime]::Now
$Variable.WebSocket = $ws
-
-
+
+ $MessageCount = [long]0
+
while ($true) {
if ($ws.State -ne 'Open') {break }
+ if ($TimeOut -and ([DateTime]::Now - $webSocketStartTime) -gt $TimeOut) {
+ $ws.CloseAsync([Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Timeout', $CT).Wait()
+ break
+ }
+
+ if ($Maximum -and $MessageCount -ge $Maximum) {
+ $ws.CloseAsync([Net.WebSockets.WebSocketCloseStatus]::NormalClosure, 'Maximum messages reached', $CT).Wait()
+ break
+ }
+
$Buf = [byte[]]::new($BufferSize)
$Seg = [ArraySegment[byte]]::new($Buf)
$null = $ws.ReceiveAsync($Seg, $CT).Wait()
- $JS = $OutputEncoding.GetString($Buf, 0, $Buf.Count)
- if ([string]::IsNullOrWhitespace($JS)) { continue }
- try {
- $webSocketMessage = ConvertFrom-Json $JS
+ $MessageCount++
+
+ try {
+ $webSocketMessage =
+ if ($Binary) {
+ $Buf -gt 0
+ } elseif ($RawText) {
+ $OutputEncoding.GetString($Buf, 0, $Buf.Count)
+ } else {
+ $JS = $OutputEncoding.GetString($Buf, 0, $Buf.Count)
+ if ([string]::IsNullOrWhitespace($JS)) { continue }
+ ConvertFrom-Json $JS
+ }
if ($handler) {
$psCmd =
if ($runspace.LanguageMode -eq 'NoLanguage' -or
@@ -214,13 +335,41 @@ function Get-WebSocket {
}
$webSocketJob.pstypenames.insert(0, 'WebSocketJob')
if ($Watch) {
- do {
+ do {
$webSocketJob | Receive-Job
Start-Sleep -Milliseconds (
7, 11, 13, 17, 19, 23 | Get-Random
)
} while ($webSocketJob.State -in 'Running','NotStarted')
- } else {
+ }
+ elseif ($WatchFor) {
+ . {
+ do {
+ $webSocketJob | Receive-Job
+ Start-Sleep -Milliseconds (
+ 7, 11, 13, 17, 19, 23 | Get-Random
+ )
+ } while ($webSocketJob.State -in 'Running','NotStarted')
+ } | . {
+ process {
+ $webSocketOutput = $_
+ foreach ($key in @($WatchFor.Keys)) {
+ $result =
+ if ($key -is [ScriptBlock]) {
+ . $key $webSocketOutput
+ }
+
+ if (-not $result) { continue }
+ if ($WatchFor[$key] -is [ScriptBlock]) {
+ $webSocketOutput | . $WatchFor[$key]
+ } else {
+ $WatchFor[$key]
+ }
+ }
+ }
+ }
+ }
+ else {
$webSocketJob
}
}
diff --git a/Container.start.ps1 b/Container.start.ps1
index dd49394..1f83e5c 100644
--- a/Container.start.ps1
+++ b/Container.start.ps1
@@ -16,7 +16,7 @@
# Run the initialization script. This will do all remaining initialization in a single layer.
RUN --mount=type=bind,src=./,target=/Initialize ./Initialize/Container.init.ps1
- ENTRYPOINT ["pwsh", "-nologo", "-file", "/Container.start.ps1"]
+ ENTRYPOINT ["pwsh", "-nologo", "-noexit", "-file", "/Container.start.ps1"]
~~~
.NOTES
Did you know that in PowerShell you can 'use' namespaces that do not really exist?
@@ -58,8 +58,13 @@ if ($args) {
}
#region Custom
else
- {
-
+ {
+ # If a single drive is mounted, start the socket files.
+ $webSocketFiles = $mountedFolders | Get-ChildItem -Filter *.WebSocket.ps1
+ foreach ($webSocketFile in $webSocketFiles) {
+ Start-ThreadJob -Name $webSocketFile.Name -ScriptBlock {param($webSocketFile) . $using:webSocketFile.FullName } -ArgumentList $webSocketFile
+ . $webSocketFile.FullName
+ }
}
#endregion Custom
}
diff --git a/README.md b/README.md
index 5b5b4f2..2180a22 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
-

+
# WebSocket
@@ -12,6 +12,14 @@ It has a single command: Get-WebSocket.
Because `Get` is the default verb in PowerShell, you can just call it `WebSocket`.
+## WebSocket Container
+
+You can use the WebSocket module within a container:
+
+~~~powershell
+docker pull ghcr.io/powershellweb/websocket
+docker run -it ghcr.io/powershellweb/websocket
+~~~
### Installing and Importing
@@ -30,4 +38,115 @@ To connect to a websocket and start listening for results, use [Get-WebSocket](G
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch
~~~
-To stop watching a websocket, simply stop the background job.
\ No newline at end of file
+To stop watching a websocket, simply stop the background job.
+
+### More Examples
+
+#### Get-WebSocket Example 1
+
+~~~powershell
+# Create a WebSocket job that connects to a WebSocket and outputs the results.
+Get-WebSocket -WebSocketUri "wss://localhost:9669/"
+~~~
+ #### Get-WebSocket Example 2
+
+~~~powershell
+# Get is the default verb, so we can just say WebSocket.
+# `-Watch` will output a continous stream of objects from the websocket.
+# For example, let's Watch BlueSky, but just the text.
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ % {
+ $_.commit.record.text
+ }
+~~~
+ #### Get-WebSocket Example 3
+
+~~~powershell
+# Watch BlueSky, but just the text and spacing
+$blueSkySocketUrl = "wss://jetstream2.us-$(
+ 'east','west'|Get-Random
+).bsky.network/subscribe?$(@(
+ "wantedCollections=app.bsky.feed.post"
+) -join '&')"
+websocket $blueSkySocketUrl -Watch |
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+~~~
+ #### Get-WebSocket Example 4
+
+~~~powershell
+websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
+~~~
+ #### Get-WebSocket Example 5
+
+~~~powershell
+# Watch BlueSky, but just the emoji
+websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+ Foreach-Object {
+ $in = $_
+ if ($in.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+') {
+ Write-Host $matches.0 -NoNewline
+ }
+ }
+~~~
+ #### Get-WebSocket Example 6
+
+~~~powershell
+$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+ Foreach-Object {
+ $in = $_
+ $spacing = (' ' * (Get-Random -Minimum 0 -Maximum 7))
+ if ($in.commit.record.text -match "(?>(?:$emojiPattern|\#\w+)") {
+ $match = $matches.0
+ Write-Host $spacing,$match,$spacing -NoNewline
+ }
+ }
+~~~
+ #### Get-WebSocket Example 7
+
+~~~powershell
+websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ Where-Object {
+ $_.commit.record.embed.'$type' -eq 'app.bsky.embed.external'
+ } |
+ Foreach-Object {
+ $_.commit.record.embed.external.uri
+ }
+~~~
+ #### Get-WebSocket Example 8
+
+~~~powershell
+# BlueSky, but just the hashtags
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+}
+~~~
+ #### Get-WebSocket Example 9
+
+~~~powershell
+# BlueSky, but just the hashtags (as links)
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ if ($psStyle.FormatHyperlink) {
+ $psStyle.FormatHyperlink($matches.0, "https://bsky.app/search?q=$([Web.HttpUtility]::UrlEncode($matches.0))")
+ } else {
+ $matches.0
+ }
+ }
+}
+~~~
+ #### Get-WebSocket Example 10
+
+~~~powershell
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$args.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+ {$args.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+'}={
+ $matches.0
+ }
+}
+~~~
+
diff --git a/README.ps.md b/README.ps.md
new file mode 100644
index 0000000..c3a7d5f
--- /dev/null
+++ b/README.ps.md
@@ -0,0 +1,61 @@
+
+

+
+
+# WebSocket
+
+Work with WebSockets in PowerShell
+
+WebSocket is a small PowerShell module that helps you work with WebSockets.
+
+It has a single command: Get-WebSocket.
+
+Because `Get` is the default verb in PowerShell, you can just call it `WebSocket`.
+
+## WebSocket Container
+
+You can use the WebSocket module within a container:
+
+~~~powershell
+docker pull ghcr.io/powershellweb/websocket
+docker run -it ghcr.io/powershellweb/websocket
+~~~
+
+### Installing and Importing
+
+~~~PowerShell
+Install-Module WebSocket -Scope CurrentUser -Force
+Import-Module WebSocket -Force -PassThru
+~~~
+
+### Get-WebSocket
+
+To connect to a websocket and start listening for results, use [Get-WebSocket](Get-WebSocket.md)
+
+~~~PowerShell
+# Because get is the default verb, we can just say `WebSocket`
+# The `-Watch` parameter will continually watch for results
+websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch
+~~~
+
+To stop watching a websocket, simply stop the background job.
+
+### More Examples
+
+~~~PipeScript{
+Import-Module .\
+Get-Help Get-WebSocket |
+ %{ $_.Examples.Example.code} |
+ % -Begin { $exampleCount = 0 } -Process {
+ $exampleCount++
+ @(
+ "#### Get-WebSocket Example $exampleCount"
+ ''
+ "~~~powershell"
+ $_
+ "~~~"
+ ''
+ ) -join [Environment]::Newline
+ }
+}
+~~~
\ No newline at end of file
diff --git a/WebSocket.psd1 b/WebSocket.psd1
index 8cdbd37..0441f61 100644
--- a/WebSocket.psd1
+++ b/WebSocket.psd1
@@ -1,5 +1,5 @@
@{
- ModuleVersion = '0.1'
+ ModuleVersion = '0.1.1'
RootModule = 'WebSocket.psm1'
Guid = '75c70c8b-e5eb-4a60-982e-a19110a1185d'
Author = 'James Brundage'
@@ -12,17 +12,28 @@
ProjectURI = 'https://github.com/PowerShellWeb/WebSocket'
LicenseURI = 'https://github.com/PowerShellWeb/WebSocket/blob/main/LICENSE'
ReleaseNotes = @'
-## WebSocket 0.1
-
> Like It? [Star It](https://github.com/PowerShellWeb/WebSocket)
> Love It? [Support It](https://github.com/sponsors/StartAutomating)
-* Initial Release of WebSocket module
- * Get-WebSocket gets content from a WebSocket
- * Docker container for WebSocket
- * Build Workflow
- * WebSocket Logo
- * WebSocket website
+## WebSocket 0.1.1
+
+* WebSocket GitHub Action
+ * Run any `*.WebSocket.ps1` files in a repository (#24)
+* WebSocket container updates
+ * Container now runs mounted `*.WebSocket.ps1` files (#26)
+* Get-WebSocket improvements:
+ * New Parameters:
+ * -Maximum (#22)
+ * -TimeOut (#23)
+ * -WatchFor (#29)
+ * -RawText (#30)
+ * -Binary (#31)
+* WebSocket Testing (#25)
+* Adding FUNDING.yml (#14)
+
+---
+
+Additional details available in the [CHANGELOG](CHANGELOG.md)
'@
}
}
diff --git a/WebSocket.tests.ps1 b/WebSocket.tests.ps1
new file mode 100644
index 0000000..19b6e82
--- /dev/null
+++ b/WebSocket.tests.ps1
@@ -0,0 +1,8 @@
+
+describe WebSocket {
+ it 'Can get websocket content' {
+ $websocketContent = @(websocket wss://jetstream2.us-east.bsky.network/subscribe -TimeOut 00:00:01 -Watch)
+
+ $websocketContent.Count | Should -BeGreaterThan 0
+ }
+}
\ No newline at end of file
diff --git a/action.yml b/action.yml
new file mode 100644
index 0000000..9f82bfe
--- /dev/null
+++ b/action.yml
@@ -0,0 +1,442 @@
+
+name: UseWebSocket
+description: Work with WebSockets in PowerShell
+inputs:
+ Run:
+ required: false
+ description: |
+ A PowerShell Script that uses WebSocket.
+ Any files outputted from the script will be added to the repository.
+ If those files have a .Message attached to them, they will be committed with that message.
+ SkipScriptFile:
+ required: false
+ description: 'If set, will not process any files named *.WebSocket.ps1'
+ InstallModule:
+ required: false
+ description: A list of modules to be installed from the PowerShell gallery before scripts run.
+ CommitMessage:
+ required: false
+ description: If provided, will commit any remaining changes made to the workspace with this commit message.
+ TargetBranch:
+ required: false
+ description: |
+ If provided, will checkout a new branch before making the changes.
+ If not provided, will use the current branch.
+ ActionScript:
+ required: false
+ description: The name of one or more scripts to run, from this action's path.
+ GitHubToken:
+ required: false
+ default: '{{ secrets.GITHUB_TOKEN }}'
+ description: The github token to use for requests.
+ UserEmail:
+ required: false
+ description: The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com.
+ UserName:
+ required: false
+ description: The user name associated with a git commit.
+ NoPush:
+ required: false
+ description: |
+ If set, will not push any changes made to the repository.
+ (they will still be committed unless `-NoCommit` is passed)
+ NoCommit:
+ required: false
+ description: |
+ If set, will not commit any changes made to the repository.
+ (this also implies `-NoPush`)
+branding:
+ icon: chevron-right
+ color: blue
+runs:
+ using: composite
+ steps:
+ - name: WebSocketAction
+ id: WebSocketAction
+ shell: pwsh
+ env:
+ CommitMessage: ${{inputs.CommitMessage}}
+ SkipScriptFile: ${{inputs.SkipScriptFile}}
+ InstallModule: ${{inputs.InstallModule}}
+ Run: ${{inputs.Run}}
+ TargetBranch: ${{inputs.TargetBranch}}
+ NoPush: ${{inputs.NoPush}}
+ GitHubToken: ${{inputs.GitHubToken}}
+ UserEmail: ${{inputs.UserEmail}}
+ NoCommit: ${{inputs.NoCommit}}
+ UserName: ${{inputs.UserName}}
+ ActionScript: ${{inputs.ActionScript}}
+ run: |
+ $Parameters = @{}
+ $Parameters.Run = ${env:Run}
+ $Parameters.SkipScriptFile = ${env:SkipScriptFile}
+ $Parameters.SkipScriptFile = $parameters.SkipScriptFile -match 'true';
+ $Parameters.InstallModule = ${env:InstallModule}
+ $Parameters.InstallModule = $parameters.InstallModule -split ';' -replace '^[''"]' -replace '[''"]$'
+ $Parameters.CommitMessage = ${env:CommitMessage}
+ $Parameters.TargetBranch = ${env:TargetBranch}
+ $Parameters.ActionScript = ${env:ActionScript}
+ $Parameters.ActionScript = $parameters.ActionScript -split ';' -replace '^[''"]' -replace '[''"]$'
+ $Parameters.GitHubToken = ${env:GitHubToken}
+ $Parameters.UserEmail = ${env:UserEmail}
+ $Parameters.UserName = ${env:UserName}
+ $Parameters.NoPush = ${env:NoPush}
+ $Parameters.NoPush = $parameters.NoPush -match 'true';
+ $Parameters.NoCommit = ${env:NoCommit}
+ $Parameters.NoCommit = $parameters.NoCommit -match 'true';
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: WebSocketAction $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {<#
+ .Synopsis
+ GitHub Action for WebSocket
+ .Description
+ GitHub Action for WebSocket. This will:
+
+ * Import WebSocket
+ * If `-Run` is provided, run that script
+ * Otherwise, unless `-SkipScriptFile` is passed, run all *.WebSocket.ps1 files beneath the workflow directory
+ * If any `-ActionScript` was provided, run scripts from the action path that match a wildcard pattern.
+
+ If you will be making changes using the GitHubAPI, you should provide a -GitHubToken
+ If none is provided, and ENV:GITHUB_TOKEN is set, this will be used instead.
+ Any files changed can be outputted by the script, and those changes can be checked back into the repo.
+ Make sure to use the "persistCredentials" option with checkout.
+ #>
+
+ param(
+ # A PowerShell Script that uses WebSocket.
+ # Any files outputted from the script will be added to the repository.
+ # If those files have a .Message attached to them, they will be committed with that message.
+ [string]
+ $Run,
+
+ # If set, will not process any files named *.WebSocket.ps1
+ [switch]
+ $SkipScriptFile,
+
+ # A list of modules to be installed from the PowerShell gallery before scripts run.
+ [string[]]
+ $InstallModule,
+
+ # If provided, will commit any remaining changes made to the workspace with this commit message.
+ [string]
+ $CommitMessage,
+
+ # If provided, will checkout a new branch before making the changes.
+ # If not provided, will use the current branch.
+ [string]
+ $TargetBranch,
+
+ # The name of one or more scripts to run, from this action's path.
+ [string[]]
+ $ActionScript,
+
+ # The github token to use for requests.
+ [string]
+ $GitHubToken = '{{ secrets.GITHUB_TOKEN }}',
+
+ # The user email associated with a git commit. If this is not provided, it will be set to the username@noreply.github.com.
+ [string]
+ $UserEmail,
+
+ # The user name associated with a git commit.
+ [string]
+ $UserName,
+
+ # If set, will not push any changes made to the repository.
+ # (they will still be committed unless `-NoCommit` is passed)
+ [switch]
+ $NoPush,
+
+ # If set, will not commit any changes made to the repository.
+ # (this also implies `-NoPush`)
+ [switch]
+ $NoCommit
+ )
+
+ $ErrorActionPreference = 'continue'
+ "::group::Parameters" | Out-Host
+ [PSCustomObject]$PSBoundParameters | Format-List | Out-Host
+ "::endgroup::" | Out-Host
+
+ $gitHubEventJson = [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH)
+ $gitHubEvent =
+ if ($env:GITHUB_EVENT_PATH) {
+ $gitHubEventJson | ConvertFrom-Json
+ } else { $null }
+ "::group::Parameters" | Out-Host
+ $gitHubEvent | Format-List | Out-Host
+ "::endgroup::" | Out-Host
+
+
+ $anyFilesChanged = $false
+ $ActionModuleName = 'PSJekyll'
+ $actorInfo = $null
+
+
+ $checkDetached = git symbolic-ref -q HEAD
+ if ($LASTEXITCODE) {
+ "::warning::On detached head, skipping action" | Out-Host
+ exit 0
+ }
+
+ function InstallActionModule {
+ param([string]$ModuleToInstall)
+ $moduleInWorkspace = Get-ChildItem -Path $env:GITHUB_WORKSPACE -Recurse -File |
+ Where-Object Name -eq "$($moduleToInstall).psd1" |
+ Where-Object {
+ $(Get-Content $_.FullName -Raw) -match 'ModuleVersion'
+ }
+ if (-not $moduleInWorkspace) {
+ $availableModules = Get-Module -ListAvailable
+ if ($availableModules.Name -notcontains $moduleToInstall) {
+ Install-Module $moduleToInstall -Scope CurrentUser -Force -AcceptLicense -AllowClobber
+ }
+ Import-Module $moduleToInstall -Force -PassThru | Out-Host
+ } else {
+ Import-Module $moduleInWorkspace.FullName -Force -PassThru | Out-Host
+ }
+ }
+ function ImportActionModule {
+ #region -InstallModule
+ if ($InstallModule) {
+ "::group::Installing Modules" | Out-Host
+ foreach ($moduleToInstall in $InstallModule) {
+ InstallActionModule -ModuleToInstall $moduleToInstall
+ }
+ "::endgroup::" | Out-Host
+ }
+ #endregion -InstallModule
+
+ if ($env:GITHUB_ACTION_PATH) {
+ $LocalModulePath = Join-Path $env:GITHUB_ACTION_PATH "$ActionModuleName.psd1"
+ if (Test-path $LocalModulePath) {
+ Import-Module $LocalModulePath -Force -PassThru | Out-String
+ } else {
+ throw "Module '$ActionModuleName' not found"
+ }
+ } elseif (-not (Get-Module $ActionModuleName)) {
+ throw "Module '$ActionModuleName' not found"
+ }
+
+ "::notice title=ModuleLoaded::$ActionModuleName Loaded from Path - $($LocalModulePath)" | Out-Host
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "# $($ActionModuleName)" |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ }
+ function InitializeAction {
+ #region Custom
+ #endregion Custom
+
+ # Configure git based on the $env:GITHUB_ACTOR
+ if (-not $UserName) { $UserName = $env:GITHUB_ACTOR }
+ if (-not $actorID) { $actorID = $env:GITHUB_ACTOR_ID }
+ $actorInfo =
+ if ($GitHubToken -notmatch '^\{{2}' -and $GitHubToken -notmatch '\}{2}$') {
+ Invoke-RestMethod -Uri "https://api.github.com/user/$actorID" -Headers @{ Authorization = "token $GitHubToken" }
+ } else {
+ Invoke-RestMethod -Uri "https://api.github.com/user/$actorID"
+ }
+
+ if (-not $UserEmail) { $UserEmail = "$UserName@noreply.github.com" }
+ git config --global user.email $UserEmail
+ git config --global user.name $actorInfo.name
+
+ # Pull down any changes
+ git pull | Out-Host
+
+ if ($TargetBranch) {
+ "::notice title=Expanding target branch string $targetBranch" | Out-Host
+ $TargetBranch = $ExecutionContext.SessionState.InvokeCommand.ExpandString($TargetBranch)
+ "::notice title=Checking out target branch::$targetBranch" | Out-Host
+ git checkout -b $TargetBranch | Out-Host
+ git pull | Out-Host
+ }
+ }
+
+ function InvokeActionModule {
+ $myScriptStart = [DateTime]::Now
+ $myScript = $ExecutionContext.SessionState.PSVariable.Get("Run").Value
+ if ($myScript) {
+ Invoke-Expression -Command $myScript |
+ . ProcessOutput |
+ Out-Host
+ return
+ }
+ $myScriptTook = [Datetime]::Now - $myScriptStart
+ $MyScriptFilesStart = [DateTime]::Now
+
+ $myScriptList = @()
+ $shouldSkip = $ExecutionContext.SessionState.PSVariable.Get("SkipScriptFile").Value
+ if ($shouldSkip) {
+ return
+ }
+ $scriptFiles = @(
+ Get-ChildItem -Recurse -Path $env:GITHUB_WORKSPACE |
+ Where-Object Name -Match "\.$($ActionModuleName)\.ps1$"
+ if ($ActionScript) {
+ if ($ActionScript -match '^\s{0,}/' -and $ActionScript -match '/\s{0,}$') {
+ $ActionScriptPattern = $ActionScript.Trim('/').Trim() -as [regex]
+ if ($ActionScriptPattern) {
+ $ActionScriptPattern = [regex]::new($ActionScript.Trim('/').Trim(), 'IgnoreCase,IgnorePatternWhitespace', [timespan]::FromSeconds(0.5))
+ Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH |
+ Where-Object { $_.Name -Match "\.$($ActionModuleName)\.ps1$" -and $_.FullName -match $ActionScriptPattern }
+ }
+ } else {
+ Get-ChildItem -Recurse -Path $env:GITHUB_ACTION_PATH |
+ Where-Object Name -Match "\.$($ActionModuleName)\.ps1$" |
+ Where-Object FullName -Like $ActionScript
+ }
+ }
+ ) | Select-Object -Unique
+ $scriptFiles |
+ ForEach-Object -Begin {
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "## $ActionModuleName Scripts" |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ } -Process {
+ $myScriptList += $_.FullName.Replace($env:GITHUB_WORKSPACE, '').TrimStart('/')
+ $myScriptCount++
+ $scriptFile = $_
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "### $($scriptFile.Fullname -replace [Regex]::Escape($env:GITHUB_WORKSPACE))" |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ $scriptCmd = $ExecutionContext.SessionState.InvokeCommand.GetCommand($scriptFile.FullName, 'ExternalScript')
+ foreach ($requiredModule in $CommandInfo.ScriptBlock.Ast.ScriptRequirements.RequiredModules) {
+ if ($requiredModule.Name -and
+ (-not $requiredModule.MaximumVersion) -and
+ (-not $requiredModule.RequiredVersion)
+ ) {
+ InstallActionModule $requiredModule.Name
+ }
+ }
+ $scriptFileOutputs = . $scriptCmd
+ $scriptFileOutputs |
+ . ProcessOutput |
+ Out-Host
+ }
+
+ $MyScriptFilesTook = [Datetime]::Now - $MyScriptFilesStart
+ $SummaryOfMyScripts = "$myScriptCount $ActionModuleName scripts took $($MyScriptFilesTook.TotalSeconds) seconds"
+ $SummaryOfMyScripts |
+ Out-Host
+ if ($env:GITHUB_STEP_SUMMARY) {
+ $SummaryOfMyScripts |
+ Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ #region Custom
+ #endregion Custom
+ }
+
+ function OutError {
+ $anyRuntimeExceptions = $false
+ foreach ($err in $error) {
+ $errParts = @(
+ "::error "
+ @(
+ if ($err.InvocationInfo.ScriptName) {
+ "file=$($err.InvocationInfo.ScriptName)"
+ }
+ if ($err.InvocationInfo.ScriptLineNumber -ge 1) {
+ "line=$($err.InvocationInfo.ScriptLineNumber)"
+ if ($err.InvocationInfo.OffsetInLine -ge 1) {
+ "col=$($err.InvocationInfo.OffsetInLine)"
+ }
+ }
+ if ($err.CategoryInfo.Activity) {
+ "title=$($err.CategoryInfo.Activity)"
+ }
+ ) -join ','
+ "::"
+ $err.Exception.Message
+ if ($err.CategoryInfo.Category -eq 'OperationStopped' -and
+ $err.CategoryInfo.Reason -eq 'RuntimeException') {
+ $anyRuntimeExceptions = $true
+ }
+ ) -join ''
+ $errParts | Out-Host
+ if ($anyRuntimeExceptions) {
+ exit 1
+ }
+ }
+ }
+
+ function PushActionOutput {
+ if ($anyFilesChanged) {
+ "::notice::$($anyFilesChanged) Files Changed" | Out-Host
+ }
+ if ($CommitMessage -or $anyFilesChanged) {
+ if ($CommitMessage) {
+ Get-ChildItem $env:GITHUB_WORKSPACE -Recurse |
+ ForEach-Object {
+ $gitStatusOutput = git status $_.Fullname -s
+ if ($gitStatusOutput) {
+ git add $_.Fullname
+ }
+ }
+
+ git commit -m $ExecutionContext.SessionState.InvokeCommand.ExpandString($CommitMessage)
+ }
+
+ $checkDetached = git symbolic-ref -q HEAD
+ if (-not $LASTEXITCODE -and -not $NoPush -and -not $noCommit) {
+ if ($TargetBranch -and $anyFilesChanged) {
+ "::notice::Pushing Changes to $targetBranch" | Out-Host
+ git push --set-upstream origin $TargetBranch
+ } elseif ($anyFilesChanged) {
+ "::notice::Pushing Changes" | Out-Host
+ git push
+ }
+ "Git Push Output: $($gitPushed | Out-String)"
+ } else {
+ "::notice::Not pushing changes (on detached head)" | Out-Host
+ $LASTEXITCODE = 0
+ exit 0
+ }
+ }
+ }
+
+ filter ProcessOutput {
+ $out = $_
+ $outItem = Get-Item -Path $out -ErrorAction Ignore
+ if (-not $outItem -and $out -is [string]) {
+ $out | Out-Host
+ if ($env:GITHUB_STEP_SUMMARY) {
+ "> $out" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
+ }
+ return
+ }
+ $fullName, $shouldCommit =
+ if ($out -is [IO.FileInfo]) {
+ $out.FullName, (git status $out.Fullname -s)
+ } elseif ($outItem) {
+ $outItem.FullName, (git status $outItem.Fullname -s)
+ }
+ if ($shouldCommit -and -not $NoCommit) {
+ "$fullName has changed, and should be committed" | Out-Host
+ git add $fullName
+ if ($out.Message) {
+ git commit -m "$($out.Message)" | Out-Host
+ } elseif ($out.CommitMessage) {
+ git commit -m "$($out.CommitMessage)" | Out-Host
+ } elseif ($gitHubEvent.head_commit.message) {
+ git commit -m "$($gitHubEvent.head_commit.message)" | Out-Host
+ }
+ $anyFilesChanged = $true
+ }
+ $out
+ }
+
+ . ImportActionModule
+ . InitializeAction
+ . InvokeActionModule
+ . PushActionOutput
+ . OutError} @Parameters
+
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 7aeb592..1020c9d 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -1,8 +1,26 @@
-## WebSocket 0.1
-
> Like It? [Star It](https://github.com/PowerShellWeb/WebSocket)
> Love It? [Support It](https://github.com/sponsors/StartAutomating)
+## WebSocket 0.1.1
+
+* WebSocket GitHub Action
+ * Run any `*.WebSocket.ps1` files in a repository (#24)
+* WebSocket container updates
+ * Container now runs mounted `*.WebSocket.ps1` files (#26)
+* Get-WebSocket improvements:
+ * New Parameters:
+ * -Maximum (#22)
+ * -TimeOut (#23)
+ * -WatchFor (#29)
+ * -RawText (#30)
+ * -Binary (#31)
+* WebSocket Testing (#25)
+* Adding FUNDING.yml (#14)
+
+---
+
+## WebSocket 0.1
+
* Initial Release of WebSocket module
* Get-WebSocket gets content from a WebSocket
* Docker container for WebSocket
diff --git a/docs/Get-WebSocket.md b/docs/Get-WebSocket.md
index a867234..5ad9b74 100644
--- a/docs/Get-WebSocket.md
+++ b/docs/Get-WebSocket.md
@@ -8,7 +8,11 @@ WebSockets in PowerShell.
### Description
-Get-WebSocket allows you to connect to a websocket and handle the output.
+Get-WebSocket gets a websocket.
+
+This will create a job that connects to a WebSocket and outputs the results.
+
+If the `-Watch` parameter is provided, will output a continous stream of objects.
---
@@ -16,14 +20,35 @@ Get-WebSocket allows you to connect to a websocket and handle the output.
Create a WebSocket job that connects to a WebSocket and outputs the results.
```PowerShell
-Get-WebSocket -WebSocketUri "wss://localhost:9669"
+Get-WebSocket -WebSocketUri "wss://localhost:9669/"
```
Get is the default verb, so we can just say WebSocket.
+`-Watch` will output a continous stream of objects from the websocket.
+For example, let's Watch BlueSky, but just the text.
+
+```PowerShell
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ % {
+ $_.commit.record.text
+ }
+```
+Watch BlueSky, but just the text and spacing
+
+```PowerShell
+$blueSkySocketUrl = "wss://jetstream2.us-$(
+ 'east','west'|Get-Random
+).bsky.network/subscribe?$(@(
+ "wantedCollections=app.bsky.feed.post"
+) -join '&')"
+websocket $blueSkySocketUrl -Watch |
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+```
+> EXAMPLE 4
```PowerShell
websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
```
-> EXAMPLE 3
+Watch BlueSky, but just the emoji
```PowerShell
websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
@@ -34,17 +59,64 @@ websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.f
}
}
```
-> EXAMPLE 4
+> EXAMPLE 6
```PowerShell
$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
-websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
Foreach-Object {
$in = $_
+ $spacing = (' ' * (Get-Random -Minimum 0 -Maximum 7))
if ($in.commit.record.text -match "(?>(?:$emojiPattern|\#\w+)") {
- Write-Host $matches.0 -NoNewline
+ $match = $matches.0
+ Write-Host $spacing,$match,$spacing -NoNewline
+ }
+ }
+```
+> EXAMPLE 7
+
+```PowerShell
+websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ Where-Object {
+ $_.commit.record.embed.'$type' -eq 'app.bsky.embed.external'
+ } |
+ Foreach-Object {
+ $_.commit.record.embed.external.uri
+ }
+```
+BlueSky, but just the hashtags
+
+```PowerShell
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+}
+```
+BlueSky, but just the hashtags (as links)
+
+```PowerShell
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ if ($psStyle.FormatHyperlink) {
+ $psStyle.FormatHyperlink($matches.0, "https://bsky.app/search?q=$([Web.HttpUtility]::UrlEncode($matches.0))")
+ } else {
+ $matches.0
}
}
+}
+```
+> EXAMPLE 10
+
+```PowerShell
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$args.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+ {$args.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+'}={
+ $matches.0
+ }
+}
```
---
@@ -66,6 +138,7 @@ A ScriptBlock that will handle the output of the WebSocket.
#### **Variable**
Any variables to declare in the WebSocket job.
+These variables will also be added to the job as properties.
|Type |Required|Position|PipelineInput|
|---------------|--------|--------|-------------|
@@ -128,6 +201,43 @@ If set, will watch the output of the WebSocket job, outputting results continuou
|----------|--------|--------|-------------|-------|
|`[Switch]`|false |named |false |Tail |
+#### **RawText**
+If set, will output the raw text that comes out of the WebSocket.
+
+|Type |Required|Position|PipelineInput|Aliases|
+|----------|--------|--------|-------------|-------|
+|`[Switch]`|false |named |false |Raw |
+
+#### **Binary**
+If set, will output the raw bytes that come out of the WebSocket.
+
+|Type |Required|Position|PipelineInput|Aliases |
+|----------|--------|--------|-------------|---------------------------------------|
+|`[Switch]`|false |named |false |RawByte
RawBytes
Bytes
Byte|
+
+#### **WatchFor**
+If set, will watch the output of a WebSocket job for one or more conditions.
+The conditions are the keys of the dictionary, and can be a regex, a string, or a scriptblock.
+The values of the dictionary are what will happen when a match is found.
+
+|Type |Required|Position|PipelineInput|Aliases |
+|---------------|--------|--------|-------------|----------------------|
+|`[IDictionary]`|false |named |false |WhereFor
Wherefore|
+
+#### **TimeOut**
+The timeout for the WebSocket connection. If this is provided, after the timeout elapsed, the WebSocket will be closed.
+
+|Type |Required|Position|PipelineInput|
+|------------|--------|--------|-------------|
+|`[TimeSpan]`|false |named |false |
+
+#### **Maximum**
+The maximum number of messages to receive before closing the WebSocket.
+
+|Type |Required|Position|PipelineInput|
+|---------|--------|--------|-------------|
+|`[Int64]`|false |named |false |
+
#### **ConnectionTimeout**
The maximum time to wait for a connection to be established.
By default, this is 7 seconds.
@@ -156,5 +266,5 @@ RunspacePools allow you to limit the scope of the handler to a pool of runspaces
### Syntax
```PowerShell
-Get-WebSocket [[-WebSocketUri] ] [-Handler ] [-Variable ] [-Name ] [-InitializationScript ] [-BufferSize ] [-OnConnect ] [-OnError ] [-OnOutput ] [-OnWarning ] [-Watch] [-ConnectionTimeout ] [-Runspace ] [-RunspacePool ] []
+Get-WebSocket [[-WebSocketUri] ] [-Handler ] [-Variable ] [-Name ] [-InitializationScript ] [-BufferSize ] [-OnConnect ] [-OnError ] [-OnOutput ] [-OnWarning ] [-Watch] [-RawText] [-Binary] [-WatchFor ] [-TimeOut ] [-Maximum ] [-ConnectionTimeout ] [-Runspace ] [-RunspacePool ] []
```
diff --git a/docs/README.md b/docs/README.md
index f263805..8ef65a9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -1,5 +1,5 @@
-

+
# WebSocket
@@ -12,6 +12,14 @@ It has a single command: Get-WebSocket.
Because `Get` is the default verb in PowerShell, you can just call it `WebSocket`.
+## WebSocket Container
+
+You can use the WebSocket module within a container:
+
+~~~powershell
+docker pull ghcr.io/powershellweb/websocket
+docker run -it ghcr.io/powershellweb/websocket
+~~~
### Installing and Importing
@@ -31,3 +39,113 @@ websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.
~~~
To stop watching a websocket, simply stop the background job.
+
+### More Examples
+
+#### Get-WebSocket Example 1
+
+~~~powershell
+# Create a WebSocket job that connects to a WebSocket and outputs the results.
+Get-WebSocket -WebSocketUri "wss://localhost:9669/"
+~~~
+ #### Get-WebSocket Example 2
+
+~~~powershell
+# Get is the default verb, so we can just say WebSocket.
+# `-Watch` will output a continous stream of objects from the websocket.
+# For example, let's Watch BlueSky, but just the text.
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ % {
+ $_.commit.record.text
+ }
+~~~
+ #### Get-WebSocket Example 3
+
+~~~powershell
+# Watch BlueSky, but just the text and spacing
+$blueSkySocketUrl = "wss://jetstream2.us-$(
+ 'east','west'|Get-Random
+).bsky.network/subscribe?$(@(
+ "wantedCollections=app.bsky.feed.post"
+) -join '&')"
+websocket $blueSkySocketUrl -Watch |
+ % { Write-Host "$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))"}
+~~~
+ #### Get-WebSocket Example 4
+
+~~~powershell
+websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post
+~~~
+ #### Get-WebSocket Example 5
+
+~~~powershell
+# Watch BlueSky, but just the emoji
+websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+ Foreach-Object {
+ $in = $_
+ if ($in.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+') {
+ Write-Host $matches.0 -NoNewline
+ }
+ }
+~~~
+ #### Get-WebSocket Example 6
+
+~~~powershell
+$emojiPattern = '[\p{IsHighSurrogates}\p{IsLowSurrogates}\p{IsVariationSelectors}\p{IsCombiningHalfMarks}]+)'
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |
+ Foreach-Object {
+ $in = $_
+ $spacing = (' ' * (Get-Random -Minimum 0 -Maximum 7))
+ if ($in.commit.record.text -match "(?>(?:$emojiPattern|\#\w+)") {
+ $match = $matches.0
+ Write-Host $spacing,$match,$spacing -NoNewline
+ }
+ }
+~~~
+ #### Get-WebSocket Example 7
+
+~~~powershell
+websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |
+ Where-Object {
+ $_.commit.record.embed.'$type' -eq 'app.bsky.embed.external'
+ } |
+ Foreach-Object {
+ $_.commit.record.embed.external.uri
+ }
+~~~
+ #### Get-WebSocket Example 8
+
+~~~powershell
+# BlueSky, but just the hashtags
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+}
+~~~
+ #### Get-WebSocket Example 9
+
+~~~powershell
+# BlueSky, but just the hashtags (as links)
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$webSocketoutput.commit.record.text -match "\#\w+"}={
+ if ($psStyle.FormatHyperlink) {
+ $psStyle.FormatHyperlink($matches.0, "https://bsky.app/search?q=$([Web.HttpUtility]::UrlEncode($matches.0))")
+ } else {
+ $matches.0
+ }
+ }
+}
+~~~
+ #### Get-WebSocket Example 10
+
+~~~powershell
+websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{
+ {$args.commit.record.text -match "\#\w+"}={
+ $matches.0
+ }
+ {$args.commit.record.text -match '[\p{IsHighSurrogates}\p{IsLowSurrogates}]+'}={
+ $matches.0
+ }
+}
+~~~
diff --git a/docs/_data/Help/Get-WebSocket.json b/docs/_data/Help/Get-WebSocket.json
index 1d24148..912942c 100644
--- a/docs/_data/Help/Get-WebSocket.json
+++ b/docs/_data/Help/Get-WebSocket.json
@@ -1,6 +1,6 @@
{
"Synopsis": "WebSockets in PowerShell.",
- "Description": "Get-WebSocket allows you to connect to a websocket and handle the output.",
+ "Description": "Get-WebSocket gets a websocket.\n\nThis will create a job that connects to a WebSocket and outputs the results.\n\nIf the `-Watch` parameter is provided, will output a continous stream of objects.",
"Parameters": [
{
"Name": null,
@@ -33,22 +33,52 @@
{
"Title": "EXAMPLE 1",
"Markdown": "Create a WebSocket job that connects to a WebSocket and outputs the results.",
- "Code": "Get-WebSocket -WebSocketUri \"wss://localhost:9669\""
+ "Code": "Get-WebSocket -WebSocketUri \"wss://localhost:9669/\""
},
{
"Title": "EXAMPLE 2",
- "Markdown": "Get is the default verb, so we can just say WebSocket.",
- "Code": "websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post"
+ "Markdown": "Get is the default verb, so we can just say WebSocket.\n`-Watch` will output a continous stream of objects from the websocket.\nFor example, let's Watch BlueSky, but just the text. ",
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |\n % { \n $_.commit.record.text\n }"
},
{
"Title": "EXAMPLE 3",
+ "Markdown": "Watch BlueSky, but just the text and spacing",
+ "Code": "$blueSkySocketUrl = \"wss://jetstream2.us-$(\n 'east','west'|Get-Random\n).bsky.network/subscribe?$(@(\n \"wantedCollections=app.bsky.feed.post\"\n) -join '&')\"\nwebsocket $blueSkySocketUrl -Watch | \n % { Write-Host \"$(' ' * (Get-Random -Max 10))$($_.commit.record.text)$($(' ' * (Get-Random -Max 10)))\"}"
+ },
+ {
+ "Title": "EXAMPLE 4",
"Markdown": "",
+ "Code": "websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post"
+ },
+ {
+ "Title": "EXAMPLE 5",
+ "Markdown": "Watch BlueSky, but just the emoji",
"Code": "websocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |\n Foreach-Object {\n $in = $_\n if ($in.commit.record.text -match '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}]+') {\n Write-Host $matches.0 -NoNewline\n }\n }"
},
{
- "Title": "EXAMPLE 4",
+ "Title": "EXAMPLE 6",
+ "Markdown": "",
+ "Code": "$emojiPattern = '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}\\p{IsVariationSelectors}\\p{IsCombiningHalfMarks}]+)'\nwebsocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |\n Foreach-Object {\n $in = $_\n $spacing = (' ' * (Get-Random -Minimum 0 -Maximum 7))\n if ($in.commit.record.text -match \"(?>(?:$emojiPattern|\\#\\w+)\") {\n $match = $matches.0 \n Write-Host $spacing,$match,$spacing -NoNewline\n }\n }"
+ },
+ {
+ "Title": "EXAMPLE 7",
+ "Markdown": "",
+ "Code": "websocket wss://jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Watch |\n Where-Object {\n $_.commit.record.embed.'$type' -eq 'app.bsky.embed.external'\n } |\n Foreach-Object {\n $_.commit.record.embed.external.uri\n }"
+ },
+ {
+ "Title": "EXAMPLE 8",
+ "Markdown": "BlueSky, but just the hashtags",
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{\n {$webSocketoutput.commit.record.text -match \"\\#\\w+\"}={\n $matches.0\n } \n}"
+ },
+ {
+ "Title": "EXAMPLE 9",
+ "Markdown": "BlueSky, but just the hashtags (as links)",
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{\n {$webSocketoutput.commit.record.text -match \"\\#\\w+\"}={\n if ($psStyle.FormatHyperlink) {\n $psStyle.FormatHyperlink($matches.0, \"https://bsky.app/search?q=$([Web.HttpUtility]::UrlEncode($matches.0))\")\n } else {\n $matches.0\n }\n }\n}"
+ },
+ {
+ "Title": "EXAMPLE 10",
"Markdown": "",
- "Code": "$emojiPattern = '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}\\p{IsVariationSelectors}\\p{IsCombiningHalfMarks}]+)'\nwebsocket jetstream2.us-east.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -Tail |\n Foreach-Object {\n $in = $_\n if ($in.commit.record.text -match \"(?>(?:$emojiPattern|\\#\\w+)\") {\n Write-Host $matches.0 -NoNewline\n }\n }"
+ "Code": "websocket wss://jetstream2.us-west.bsky.network/subscribe?wantedCollections=app.bsky.feed.post -WatchFor @{\n {$args.commit.record.text -match \"\\#\\w+\"}={\n $matches.0\n }\n {$args.commit.record.text -match '[\\p{IsHighSurrogates}\\p{IsLowSurrogates}]+'}={\n $matches.0\n }\n}"
}
]
}
\ No newline at end of file
diff --git a/docs/_data/LastDateBuilt.json b/docs/_data/LastDateBuilt.json
index 96642b6..8dd806a 100644
--- a/docs/_data/LastDateBuilt.json
+++ b/docs/_data/LastDateBuilt.json
@@ -1 +1 @@
-"2024-11-27"
\ No newline at end of file
+"2024-12-04"
\ No newline at end of file