obs-powershell.build.ps1
#requires -Module PipeScript # The WebSocket is nice enough to provide it's documentation in JSON $obsWebSocketProtocol = Invoke-RestMethod https://raw.githubusercontent.com/obsproject/obs-websocket/master/docs/generated/protocol.json # This will save us lots of time and effort for parsing # We'll want to translate OBS WebSocket requests into commands. # We'll want a prefix on all commands. $modulePrefix = 'OBS' # And we'll want a hashtable of replacements # A number of request types start with a verb name already $verbReplacements = @{} # these are easy foreach ($easyVerb in 'Get', 'Set','Open', 'Close', 'Start', 'Stop', 'Resume','Remove','Save','Send') { $verbReplacements[$easyVerb] = $easyVerb } # A number of other words cases should also infer the verb $verbReplacements += [Ordered]@{ 'Toggle' = 'Switch' # Toggle infers switch 'Create' = 'Add' # Create infers add 'Duplicate' = 'Copy' # Duplicate infers copy 'Broadcast' = 'Send' # Broadcast infers Send 'List' = 'Get' # List infers get (but may cause duplication problems) } # Construct a pair of regex to see if something starts or ends with our replacements $startsWithVerbRegex = "^(?>$($verbReplacements.Keys -join '|'))" $endsWithVerbRegex = "(?>$($verbReplacements.Keys -join '|'))$" # We also want a pair of regexes to determine if a value has a min/max range. $minRangeRestriction = "\>=\s{0,}(?<min>[\d\.-]+)" $maxRangeRestriction = "\<=\s{0,}(?<max>[\d\.-]+)" # Create an array to hold all of the functions we create $obsFunctions = @() # and files we build. $filesBuilt = @() # And determine where we want to store them $commandsPath = Join-Path $PSScriptRoot Commands $requestsPath = Join-Path $commandsPath Requests # (create the directory if it didn't already exist) if (-not (Test-Path $requestsPath)) { $null = New-Item -ItemType Directory -Path $requestsPath -Force } # Declare the process block for all commands now $obsFunctionProcessBlock = { # Create a copy of the parameters (that are part of the payload) $paramCopy = [Ordered]@{} # get a reference to this command $myCmd = $MyInvocation.MyCommand # Keep track of how many requests we have done of a given type # (this makes creating RequestIDs easy) if (-not $script:ObsRequestsCounts) { $script:ObsRequestsCounts = @{} } # Set my requestType to blank $myRequestType = '' # and indicate we are not expecting a response $responseExpected = $false # Then walk over this commands' attributes, foreach ($attr in $myCmd.ScriptBlock.Attributes) { if ($attr -is [Reflection.AssemblyMetadataAttribute]) { if ($attr.Key -eq 'OBS.WebSocket.RequestType') { $myRequestType = $attr.Value # set the requestType, } elseif ($attr.Key -eq 'OBS.WebSocket.ExpectingResponse') { # and determine if we are expecting a response. $responseExpected = if ($attr.Value -eq 'false') { $false } else { $true } } } } # Walk over each parameter :nextParam foreach ($keyValue in $PSBoundParameters.GetEnumerator()) { # and walk over each of it's attributes to see if it part of the payload foreach ($attr in $myCmd.Parameters[$keyValue.Key].Attributes) { # If the parameter is bound to part of the payload if ($attr -is [ComponentModel.DefaultBindingPropertyAttribute]) { # copy it into our payload dicitionary. $paramCopy[$attr.Name] = $keyValue.Value # (don't forget to turn switches into booleans) if ($paramCopy[$attr.Name] -is [switch]) { $paramCopy[$attr.Name] = [bool]$paramCopy[$attr.Name] } continue nextParam } } } # If we don't have a request counter for this request type if (-not $script:ObsRequestsCounts[$requestType]) { # initialize it to zero. $script:ObsRequestsCounts[$requestType] = 0 } # Increment the counter for requests of this type $script:ObsRequestsCounts[$requestType]++ # and make a request ID from that. $myRequestId = "$myRequestType.$($script:ObsRequestsCounts[$requestType])" # Construct the actual payload $payloadJson = [Ordered]@{ op = 6 # All requests are sent with the opcode 6 d = @{ # and must include a request ID requestId = "$myRequestType.$($script:ObsRequestsCounts[$requestType])" # request type requestType = $myRequestType # and optional data requestData = $paramCopy } } | # Once we have constructed the payload, make it JSON ConvertTo-Json -Depth 100 # And create a byte segment to send it offf. $SendSegment = [ArraySegment[Byte]]::new([Text.Encoding]::UTF8.GetBytes($PayloadJson)) # If we have no OBS connections if (-not $script:ObsConnections.Values) { # error out Write-Error "Not connected to OBS. Use Connect-OBS." return } # Otherwise, walk over each connection foreach ($obsConnection in $script:ObsConnections.Values) { $OBSWebSocket = $obsConnection.Websocket if ($VerbosePreference -notin 'silentlyContinue', 'ignore') { Write-Verbose "Sending $payloadJSON" } # and send the payload $null = $OBSWebSocket.SendAsync($SendSegment,'Text', $true, [Threading.CancellationToken]::new($false)) # If a response was expected if ($responseExpected) { # wait a second for that event $eventResponse = Wait-Event -SourceIdentifier $myRequestId -Timeout 1 | Select-Object -ExpandProperty MessageData # Collect all properties from the response $eventResponseProperties = @($eventResponse.psobject.properties) $expandedResponse = # If there was only one, expand that property if ($eventResponseProperties.Length -eq 1) { $eventResponse.psobject.properties.value } else { $eventResponse } # Now walk thru each response and expand / decorate it foreach ($responseObject in $expandedResponse) { # If there was no response, move on. if ($null -eq $responseObject) { continue } # If the response is a string and it's the same as the request type if ($responseObject -is [string] -and $responseObject -eq $myRequestType) { continue # ignore it } # otherwise, if the response looks like a file elseif ($responseObject -is [string] -and $responseObject -match '^(?:\p{L}\:){0,1}[\\/]') { $fileName = $responseObject -replace '[\\/]', ([io.path]::DirectorySeparatorChar) if (Test-Path $fileName) { $responseObject = Get-Item -LiteralPath $fileName } } # Otherwise, create a new PSObject out of the response $responseObject = [PSObject]::new($responseObject) # and decorate it with the command name and OBS.requestype.response $responseObject.pstypenames.add("$myCmd") $responseObject.pstypenames.add("OBS.$myRequestType.Response") # Now, walk thru all properties in our input payload foreach ($keyValue in $paramCopy.GetEnumerator()) { # If they were not in our output if (-not $responseObject.psobject.properties[$keyValue.Key]) { # add them $responseObject.psobject.properties.add( [psnoteproperty]::new($keyValue.Key, $keyValue.Value) ) } # Doing this will make it easier to pipe one step to another # and make results more useful. } # finally, emit our response object $responseObject } } } } # Walk over each type of request. foreach ($obsRequestInfo in $obsWebSocketProtocol.requests) { $requestType = $obsRequestInfo.RequestType $replacedRequestType = "$requestType" # Determine the function name $obsFunctionName = @( # If it started with something that inferred the verb if ($requestType -match $startsWithVerbRegex) { # replace the name $replacedRequestType = ($replacedRequestType -replace $startsWithVerbRegex) $verbReplacements[$matches.0] + '-' + $modulePrefix + $replacedRequestType } # If it ended with something that inferred the verb if ($requestType -cmatch $endsWithVerbRegex) { # replace the name again $replacedRequestType = ($replacedRequestType -creplace $endsWithVerbRegex) $verbReplacements[$matches.0] + '-' + $modulePrefix + $replacedRequestType } # If it didn't start or end with something that inferred a verb if ($requestType -notmatch $startsWithVerbRegex -and $requestType -notmatch $endsWithVerbRegex) { # Use Send- "Send-${modulePrefix}${RequestType}" })[-1] # Pick the last output for from this set of options, as this will be the most changed. $obsFunctionParameters = [Ordered]@{} # Now we have to turn each field in the request into a parameter foreach ($requestField in $obsRequestInfo.requestFields) { $valueType = $requestField.valueType # Some field names contain periods, don't forget to get rid of those. $paramName = $requestField.valueName -replace '\.' $paramType = # map their parameter types to PowerShell parameter types if ($valueType -eq 'Boolean') { '[switch]'} elseif ($valueType -eq 'Number') { '[double]'} elseif ($valueType -eq 'Object') { '[PSObject]'} elseif ($valueType -eq 'Any') { '[PSObject]'} elseif ($valueType -eq 'String') { '[string]'} else { '' } # And declare a parameter $obsFunctionParameters[$paramName] = @( # Include the description "<# $($requestField.ValueDescription) #>" # Make sure to declare it as ValueFromPipelineByPropertyName "[Parameter($( # and mark it as mandtory if it's not optional. if (-not $requestField.ValueOptional) { "Mandatory,"} )ValueFromPipelineByPropertyName)]" # Track the 'bound' property "[ComponentModel.DefaultBindingProperty('$($requestField.valueName)')]" # If there were range descriptions if ($requestField.valueRestrictions) { # determine the min/max $rangeMin, $rangeMax = $null, $null if ($requestField.valueRestrictions -match $minRangeRestriction) { $rangeMin = [double]$matches.min } if ($requestField.valueRestrictions -match $maxRangeRestriction) { $rangeMax = [double]$matches.max } # and write a [ValidateRange()] if ($rangeMin -ne $null -or $rangeMax -ne $null) { "[ValidateRange($($rangeMin),$(if ($rangeMax) { $rangeMax } elseif ([Math]::Round($rangeMin) -eq $rangeMin) { "[int]::MaxValue" } else { "[double]::MaxValue" }))]" } } # Include the parameter type $paramType # and declare the parameter. "`$$paramName" ) } $newFunc = New-PipeScript -FunctionName $obsFunctionName -Parameter $obsFunctionParameters -Process $obsFunctionProcessBlock -Attribute @" [Reflection.AssemblyMetadata('OBS.WebSocket.RequestType', '$requestType')] $( if ($obsRequestInfo.responseFields.Count) { "[Reflection.AssemblyMetadata('OBS.WebSocket.ExpectingResponse', `$true)]" } ) "@ -Synopsis " $obsFunctionName : $requestType " -Description @" $($obsRequestInfo.description) $obsFunctionName calls the OBS WebSocket with a request of type $requestType. "@ -Example @( if ($obsRequestInfo.requestFields.Count -eq 0) { "$obsFunctionName" } ) -Link @( "https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#$($requestType.ToLower())" ) # If no function was created, something went wrong if (-not $newFunc) { # so leave this no-op in here to be able to easily debug. $null = $null } else { # Otherwise, write the ful $outputPath = Join-Path $requestsPath "$obsFunctionName.ps1" $newFunc | Set-Content $outputPath -Encoding utf8 $builtFile = Get-Item -LiteralPath $outputPath # and attach information about what was generated (in case of collision) $builtFile | Add-Member NoteProperty OBSRequestType $obsRequestInfo.requestType -Force -PassThru | Add-Member NoteProperty OBSRequestInfo $obsRequestInfo -Force -PassThru | Add-Member NoteProperty Contents "$newFunc" -Force $filesBuilt += $builtFile $obsFunctions += $newFunc } } # Now group all the files $filesBuilt | Group-Object | ForEach-Object { # If it only created it once if ($_.Count -eq 1) { return $_.Group # return it directly } # Otherwise, basically do a simplified rename on each file $groupOfDuplicates = $_.Group $groupInfo = $_ # start by removing our current name Remove-Item $groupOfDuplicates[0].FullName # Then, for each duplicate foreach ($file in $groupOfDuplicates) { # Figure out the name it thought it had $functionName = $file.Name -replace '\.ps1$' # And the underlying request type $requestType = $file.OBSRequestType # use that to re-determine the function name $newFunctionName = if ($requestType -match $startsWithVerbRegex) { $verbReplacements[$matches.0] + '-' + $modulePrefix + ($RequestType -replace $startsWithVerbRegex) } else { "Send-${modulePrefix}${RequestType}" } # and use that name to determine a new path $newPath = Join-Path $file.Directory "$newFunctionName.ps1" # Then replace the function name within it's contents. $file.contents -replace $functionName, $newFunctionName | Set-Content -LiteralPath $newPath -Encoding utf8 # And return the new file, with the same information set attached. Get-Item -LiteralPath $newPath | Add-Member NoteProperty OBSRequestType $file.requestType -Force -PassThru | Add-Member NoteProperty OBSRequestInfo $file.obsRequestInfo -Force -PassThru | Add-Member NoteProperty Contents $file.Contents -Force -PassThru } } |