diff --git a/Out-IrcBot.ps1 b/Out-IrcBot.ps1 new file mode 100644 index 0000000..961a9fb --- /dev/null +++ b/Out-IrcBot.ps1 @@ -0,0 +1,44 @@ +<# +.Synopsis + Reads text from the pipeline, and sends it to an IRC channel + via a named pipe -- to --> Run-IrcBot.ps1 +.DESCRIPTION + Long description +.EXAMPLE + PS C:\> 'Hello World' | .\Out-IrcBot.ps1 +.EXAMPLE + PS C:\> Get-Content test.txt | .\Out-IrcBot.ps1 +#> +[CmdletBinding()] +[Alias()] +Param +( + # The text to send to IRC + [Parameter(Mandatory=$true, + ValueFromPipeline=$true)] + [String]$Text +) + +Process +{ + # Create and connect to a pipe + # (This end is asynchronous and message based because the other end needs to be, and this matches it) + # Pipe is 'InOut' direction because if you just specify a one-way pipe, you can't + # make it 'Message' type, unless you manually specify the permissions. This is easier. + Write-Host -ForegroundColor Cyan "Writing: $Text" + + $pipe = New-Object System.IO.Pipes.NamedPipeClientStream('.', #computer + 'ircbot_pipe', #pipe name + [System.IO.Pipes.PipeDirection]::InOut, + [System.IO.Pipes.PipeOptions]::Asynchronous) + + + $pipe.Connect(10000) #10,000 milisecond connection timeout + $pipe.ReadMode = [System.IO.Pipes.PipeTransmissionMode]::Message # can't specify in ctor + + # Convert the text to a byte array and send it + $Message = [System.Text.Encoding]::UTF8.GetBytes($Text) + $pipe.write($Message, 0, $Message.Length) + + $pipe.Dispose() +} diff --git a/README.md b/README.md index 1df3d34..bb593a8 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,20 @@ You can also use the command-line option to use another script or pass arguments .\Run-IrcBot.ps1 awesomebot ircserver channel superbot .\Run-IrcBot.ps1 awesomebot ircserver channel { .\superbot.ps1 $Message $Bot -DoAwesomeStuff } ``` +### Sending text from PowerShell to IRC + +Use this to make the bot 'speak' in IRC, triggered by a local PowerShell script. +e.g. once the bot is running, open a new PowerShell window and: + +```PowerShell +echo 'Hello World!' | .\Out-IrcBot.ps1 +``` +The bot will say 'Hello World' into the first IRC channel it's joined to. You could use this to notify an IRC channel of the results of a command: + +```PowerShell +Get-Content test.txt | .\Out-IrcBot.ps1 +``` +This works by Run-IrcBot.ps1 keeping a named pipe open, and Out-IrcBot.ps1 reading from the pipeline and writing to the named pipe. ## Specification diff --git a/Run-IrcBot.ps1 b/Run-IrcBot.ps1 index 1edb276..55febec 100644 --- a/Run-IrcBot.ps1 +++ b/Run-IrcBot.ps1 @@ -705,6 +705,105 @@ function Run-Bot ($line, $bot, [switch]$fatal) $bot.CurrentError = $null } +function Handle-InputPipeStateMachine ($bot) +{ + # Keeps a named pipe open on the local computer. + # Other PowerShell cmdlets can write text to it using | Out-IrcBot.ps1 + # and this reads it and sends it on to IRC through the $bot + + # Is Asynchronous to make it non-blocking. Lifecycle is: + # 1. Create a named pipe. + # . Initialize variables for one message + # 3. Async wait for connections + # 4. Check if a connection happened, if not stay here at 4. + # 5. Start an async Read(). + # 6. if read finished, add text to array. If not, stay here at 6. + # 7. Check if Pipe Message is completed. If not, goto 5. + # . All text read, pipe message complete. Write text to IRC. Goto 2. + + # Init + if ($null -eq $Script:InputPipeState) { + $Script:InputPipeState = 1 + } + + # State machine + switch ($Script:InputPipeState) + { + 1 { + $Script:InputPipe = new-object System.IO.Pipes.NamedPipeServerStream('ircbot_pipe', + [System.IO.Pipes.PipeDirection]::InOut, + 1, #? idk what this is for, just copypasted it + [System.IO.Pipes.PipeTransmissionMode]::Message, + [System.IO.Pipes.PipeOptions]::Asynchronous) + + $Script:InputPipeMessageBuffer = New-Object byte[] 1024 #1Kb read buffer + $Script:InputPipeMessageBuilder = New-Object System.Text.StringBuilder + + $Script:InputPipeState = 3 + } + + 3 { + $Script:InputPipeConnectionWait = $Script:InputPipe.WaitForConnectionAsync() # wait for client + + $Script:InputPipeState = 4 + } + + 4 { + if ($Script:InputPipeConnectionWait.IsCompleted) { # client connected + $Script:InputPipeState = 5 + } + } + + 5 { + $Script:InputPipeReadWait = $Script:InputPipe.ReadAsync($Script:InputPipeMessageBuffer, #begin reading from pipe into buffer + 0, #store at buffer byte 0 + $Script:InputPipeMessageBuffer.Length) #max num bytes to read + $Script:InputPipeState = 6 + } + + 6 { + if ($Script:InputPipeReadWait.IsCompleted) { # background read finished + $NumBytesRead = $Script:InputPipeReadWait.Result + $MessageText = [System.Text.Encoding]::UTF8.GetString($Script:InputPipeMessageBuffer, 0, $NumBytesRead) + $null = $Script:InputPipeMessageBuilder.Append($MessageText) + + $Script:InputPipeState = 7 + } + } + + 7 { + if (-not $Script:InputPipe.IsMessageComplete) + { + $Script:InputPipeState = 5 # read again + } + else + { + $line = $Script:InputPipeMessageBuilder.ToString() + $target = @($bot.Channels)[0] + $line = "PRIVMSG $target :$line" + + if ($bot.Writer) { + $bot.Writer.WriteLine($line) + $bot.Writer.Flush() + } + + $Script:InputPipe.Close() + $Script:InputPipe.Dispose() + $Script:InputPipeState = 1 # restart + } + } + + } + +} + +function Handle-InputPipe ($bot) { + # As the main loop has a delay in it, this runs quickly through the entire state machine each call + for ($i=0; $i -lt 7; $i++) { + Handle-InputPipeStateMachine $bot + } +} + function Main { try @@ -782,6 +881,8 @@ function Main while ($bot.Running) { + Handle-InputPipe $bot + if ($active) { sleep -Milliseconds $bot.InteractiveDelay @@ -843,6 +944,7 @@ function Main } finally { + if ($bot.Connection) { $bot.Connection.Close() @@ -850,6 +952,13 @@ function Main Write-BotHost "Disconnected [$([DateTime]::Now.ToString())]`n" } + + if ($Script:InputPipe) + { + $Script:InputPipe.Close() + $Script:InputPipe.Dispose() + } + } }