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 Logo (Animated) + WebSocket Logo (Animated)
# 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 Logo (Animated) +
+ +# 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 Logo (Animated) + WebSocket Logo (Animated)
# 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