There are a few programming projects that just have to be re-written in each and every programming language. You might think it’s a waste of time, but because they’re used primarily by programmers and geeks, and because using them usually involves heavily modifying them, everyone wants one that was written in the language (s)he’s most familiar with, and writing them can be a good learning experience. One such project is the IRC bot.

I wrote a bot in PowerShell awhile back that we called PowerBot … the script using XMPP (Jabber), and could connect to IRC through a gateway. Although it could do the thing a bot should be able to do (join channels, respond to messages, etc), and had bonus features like announcing new posts from PoshCode.org, and following channel members on Twitter … it was technically a Jabber bot, not an IRC bot (and thus, a little slow to respond on IRC).

In any case, I’ve created a very simple framework for people to write PowerShell IRC decided to rectify that now by publishing a new bot based on the SmartIrc4Net library. Of course, it’s not a “pure” PowerShell bot (since I’m using an IRC library), but this is .Net after all, and we don’t really believe in rewriting things that already work just fine. SmartIrc4net is a very nice bit of work, and it’s a single dll you just have to plop in a folder with the script, and it’s LGPL (2.1) so you can reuse it at will with very few restrictions. :)

The Script, Explained

The full script is available on PoshCode but I wanted to go through and explain a few pieces of it, so I’ll break it up in pieces here. Incidentally, if you run this command on that version of the script, you’ll see there are exactly 39 lines of code, including loading the irc library, and the initial Hello World command.


gc PowerBot.ps1 |?{$_ -notmatch "^\s*#" -and $_.trim().length} | measure-object -line -word

The first section of the script loads the assembly and contains the Start-PowerBot script. I’ve wrapped things onto multiple lines here to fit in my blog a bit better and for readability:


## Add-Type -path $ProfileDir\Libraries\Meebey.SmartIrc4net.dll
$null = [Reflection.Assembly]::LoadFrom("$ProfileDir\Libraries\Meebey.SmartIrc4net.dll")

function Start-PowerBot {
PARAM(
  $server = "irc.freenode.net"
, [string[]]$channels = @("#PowerShell")
, [string[]]$nick     = @(Read-Host 'You must provide a nickname')
, [string]$password
, $realname           = "PowerShell Bot"
, $port               = 6667
)
   
   if(!$global:irc) {
      $global:irc = New-Object Meebey.SmartIrc4net.IrcClient
      $irc.ActiveChannelSyncing = $true # $irc will track channels for us
      # $irc.Encoding = [Text.Encoding]::UTF8
      $irc.Add_OnError( {Write-Error $_.ErrorMessage} )
      $irc.Add_OnQueryMessage( {PrivateMessage} )
      $irc.Add_OnChannelMessage( {ChannelMessage} )
   }
   
   $irc.Connect($server, $port)
   $irc.Login($nick, $realname, 0, $nick, $password)
   ## $channels | % { $irc.RfcJoin( $_ ) }
   foreach($channel in $channels) { $irc.RfcJoin( $channel ) }
   Resume-PowerBot # Shortcut so starting this thing up only takes one command
}

The most important thing to notice here is that the SmartIrc4Net library is event driven, so you just have to Add scriptblock event handlers to things, and it will call them when something happens. You can see I’ve handled the error condition directly in the scriptblock, but for the other handlers I’ve called out functions which I will specify later.

In this simple bot I’ve only handled errors, private messages (a QueryMessage is a message sent directly to the bot, instead of to the channel), and channel messages. The “channel” is where you all hang out and chat — you young’uns may know this as a “chat room” ... but on IRC we still like to pretend we’re on HAM radio, so we have channels and go by weird aliases like “Gaurhoth” and “Gnopeg” and “SmellyHippy” and consider it impolite to ask what people’s real names are (we’ll probably kick-ban you if you ask “A/S/L?”).

You do NOT need a password for the $irc.login call, but if you have registered your bot’s nick (which I highly recommend if your IRC network supports it), then you should pass it’s password to the function. Finally, the Resume-Powerbot call at the end is a call to our simplest function. This is essentially the “message loop” of our program:


## Note that PowerBot stops listening if you press a key ...
## You'll have to re-run Resume-Powerbot to get him to listen again
function Resume-PowerBot {
   while(!$Host.UI.RawUI.KeyAvailable) { $irc.ListenOnce($false) }
}

function Stop-PowerBot ($msg="If people listened to themselves more often, they would talk less."){
   $irc.RfcQuit($msg)
   $irc.Disconnect()
}

Resume-PowerBot is the simplest possible loop: it will exit if you press any key while the PowerShell window has focus. You might want to beef up the loop, and add the ability to type while the script is running (see my demo script). Putting that together with this would let you implement a simple interactive PowerShell IRC client … which is scriptable to the Nth degree.

When I call ListenOnce($false) I pass the optional boolean parameter $false to specify that I don’t want ListenOnce to block — otherwise, ListenOnce() will block until it actually receives a message: it’s basically like the difference between writing if($Host.UI.RawUI.KeyAvailable){$Host.UI.RawUI.ReadKey()} and just writing $Host.UI.RawUI.ReadKey()

Stop-PowerBot actually sends a quite message and then disconnects from IRC, and if you call it, you’ll need to call Start-PowerBot before you can actually use the $irc variable again…

Event handlers

Event handlers in PowerShell are really very frustrating. Instead of just passing the arguments to the event handler, thus allowing your event handler to match “any” event signature, PowerShell takes exactly two arguments to it’s event handlers: the first is the $this parameter which represents what is triggering the event, and the second is the $_ parameter which represents the EventArgs. In any case, although that is sometimes limiting when developer use non-standard events, it does represent the usual CLR-compliant event signature, and luckily, most of the events in SmartIrc4net match it.

The two handlers I’ve written in this demo handle private (Query) messages and public channel messages. They are virtually identical, except that they send the return message differently :)


function PrivateMessage {
   $Data = $_.Data
   # Write-Verbose $($Data | Out-String)
   
   $command, $params = $Data.MessageArray
   if($PowerBotCommands.ContainsKey($command)) {
      &$PowerBotCommands[$command] $params $Data |
         Out-String -width (510 - $Data.From.Length - $nick.Length - 3) |
            % { $_.Trim().Split("`n") | %{ $irc.SendMessage("Message", $Data.From, $_.Trim() ) }}
   }
}

function ChannelMessage {
   $Data = $_.Data
   # Write-Verbose $($Data | Out-String)
   
   $command, $params = $Data.MessageArray
   if($PowerBotCommands.ContainsKey($command)) {
      &$PowerBotCommands[$command] $params $Data |
         Out-String -width (510 - $Data.Channel.Length - $nick.Length - 3) |
            % { $_.Trim().Split("`n") | %{ $irc.SendMessage("Message", $Data.Channel, $_.Trim() ) }}
   }
}

Finally, the actual commands for this bot are stored as a hash. You “register” a command by adding a key to the hash, where the key is a single word that matches the hash (it has to be the first word in the message) and the value is a scriptblock that will be executed. Generally, you just need to return a string (or an object) that you want to reply with, but you can, obviously, take any action you want.

Unlike the event handlers, the powerbot commands receive parameters: the first is a convenience parameter: it is the rest of the message that came after the keyword. The second parameter is the full “Data” member from SmartIrc4Net, and includes all the text that was in the message. Although the first parameter is a little redundant, most commands only need to handle the first parameter, so I’m keeping it there.


$PowerBotCommands=@{}

## A simple Hello World -like command to get you started:
$PowerBotCommands."Hello" = {Param($Query,$Data)
   "Hello, $($Data.Nick)!"
}

## A scary feature that lets you use the bot as a mannequin:
$PowerBotCommands."!Echo" = {Param($Query,$Data)
   "$Query"
}

## Executing PowerShell cmdlets in response to a spoken command:
$PowerBotCommands."!Get-Help" = {Param($Query)
   $help = get-help $Query | Select Name,Synopsis,Syntax
   if($?) {
      if($help -is [array]) {
         "You're going to need to be more specific, I know all about $((($help | % { $_.Name })[0..($help.Length-2)] -join ', ') + ' and even ' + $help[-1].Name)"
      } else {
         @($help.Synopsis,($help.Syntax | Out-String -width 1000).Trim().Split("`n",4,"RemoveEmptyEntries")[0..3])
      }
   } else {
      "I couldn't find the help file for '$Query', sorry.  I probably don't have that snapin loaded."
   }
}
Reblog this post [with Zemanta]

Comments are closed.