Posts Tagged ‘Chat’

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]

Someone asked on the PowerShell Community Forum about a way to use google talk or jabber to send messages from PowerShell, and I got carried away …

The result is yet another CodePlex project: PoshXmpp. Basically I just took the event-based GPL library provided by AG Software (agsXMPP) and wrote a polling-based wrapper for it.

You can do pretty much anything instant-messaging related that you want to do with Jabber, from building bridges between different jabber transports, to notifying yourself of events on servers, etc. As an example, here’s the script for a Jabber bot that joins a chat (#PowerShell on irc.FreeNode.net by default) and then notifies us of new posts in an ATOM feed (by default, it monitors the PowerShell usenet newsgroup by using the Google groups ATOM feed).


param (
    $JabberId = $( Read-Host "Bot's Jabber ID" )
   ,$Password = $( Read-Host "Bot's Password" -asSecure)
   ,$AtomFeeds[] = @("http://groups.google.com/group/microsoft.public.windows.powershell/feed/atom_v1_0_topics.xml")
   ,$Chat = "PowerShell%irc.FreeNode.net@irc.im.flosoft.biz"     # An IRC channel to join!
   ,$ChatNick = $("PowerBot$((new-object Random).Next(0,9999))") # Your nickname in IRC
)
$ErrorActionPreference = "Stop"

$global:PoshXmppClient =
PoshXmpp\New-Client $JabberId $Password # http://im.flosoft.biz:5280/http-poll/
PoshXmpp\Connect-Chat $Chat $ChatNick

$nnc = $global:LastNewsCheck = [DateTime]::Now.AddHours(-10) # start
$feedReader = new-object Xml.XmlDocument

"PRESS ANY KEY TO STOP"
while(!$Host.UI.RawUI.KeyAvailable) {
   "Checking feeds..."
   foreach($feed in $AtomFeeds) {
      $feedReader.Load($feed)
      for($i = $feedReader.feed.entry.count - 1; $i -ge 0; $i--) {
         $e = $feedReader.feed.entry[$i]
         if([datetime]$e.updated -gt $global:LastNewsCheck) {
            PoshXmpp\Send-Message $Chat $("{0} {1} (Posted at {2:hh:mm} by {3})" `
               -f $e.title."#text",
                  $e.link.href,
                  [datetime]$e.updated,
                  $e.author.name)
            [Threading.Thread]::Sleep( 1000 )
         }
      }
      if( [datetime]$feedReader.feed.entry[0].updated -gt $nnc ) {
         $nnc = [datetime]$feedReader.feed.entry[0].updated
      }
   }
   $global:LastNewsCheck = $nnc # the most recent item in any feed
   $counter = 0
   "PRESS ANY KEY TO STOP" # we're going to wait 10 * 60 seconds
   while(!$Host.UI.RawUI.KeyAvailable -and ($counter++ -lt 600)) {
      [Threading.Thread]::Sleep( 1000 )
   }
}

$global:PoshXmppClient.Close()

Now, most of that script was spent parsing xml from the atom feed, so lets try another example. This script will join mirror a groupt chat to instant message. By default it joins the PowerShell IRC channel, and then instant messages you so that you can participate in the IRC chat via your instant messenger. You can register your Jabber account with the AIM, ICQ, MSN, or other transport on it’s Jabber server and then use this to mirror IRC to your MSN account or to AIM on your phone or whatever ;-) . I’ve been using im.flosoft.biz which allows you to not only register new accounts on the web, but also register them with the transports through a web form.


param (
    $JabberId = $( Read-Host "Bot's Jabber ID" )
   ,$Password = $( Read-Host "Bot's Password" -asSecure)
   ,$MirrorTo = $( Read-Host "Your Jabber ID" )                  # You can use Jabber Transport Id's too
   ,$Chat = "PowerShell%irc.FreeNode.net@irc.im.flosoft.biz"     # An IRC channel to join!
   ,$ChatNick = $("PowerBot$((new-object Random).Next(0,9999))") # Your nickname in IRC
)

$global:PoshXmppClient =
PoshXmpp\New-Client $JabberId $Password # http://im.flosoft.biz:5280/http-poll/
PoshXmpp\Connect-Chat $Chat $ChatNick
PoshXmpp\Send-Message $MirrorTo "Starting Jabber Mirror to $('{0}@{1}' -f $Chat.Split(@('%','@'),3))"

"PRESS ANY KEY TO STOP"
while(!$Host.UI.RawUI.KeyAvailable) {
   PoshXmpp\Receive-Message -All | foreach-object {
      if( $_.From.Bare -ne $MirrorTo ) {
         PoshXmpp\Send-Message $MirrorTo ("{0}<{1}> {2}" -f `
            ($_.From.User -split "%")[0],
             $_.From.Resource,
             $_.Body)
      } else {
         PoshXmpp\Send-Message $Chat $_.Body
      }
   }  
   [Threading.Thread]::Sleep( 100 )
}

PoshXmpp\Disconnect-Chat $Chat $ChatNick
$global:PoshXmppClient.Close();

There’s another example on the PowerShell Xmpp for Jabber project page, showing how you can use this to read a chat room out loud (sometimes I run that when I’m just monitoring a chat room while I do other work). You could also use the Xmpp library for notifications — set up your Windows 2008 server to instant message you when IIS fails, or when you receive an email, whatever. I’ve even been playing with Invoke-Expression … you can actually write a script to do powershell remoting over Jabber (yikes).

Search My Content