Wednesday, January 12, 2011

Real-Time Solutions via ScalaBot

I was talking about a pseudo hot swapping solution in my last post. I briefly spoke of its potential, but at the time of its writing, I wasn't even sure it would solve problems. I decided a good way for me to find out if it was capable of solving those problems, would be to build something with it.

The first decent scenario I could think of, was to program a little ircbot whose behavior I could change on the fly. The more I thought it, the more I was convinced that it would be a perfect fit. So here's the spiel:

Let's say I want to read the #scala irc logs every day. Yes, I'm that much of a dork. This much of the requirement I knew into building the program, and hurried to launch the app. Well, after three days of reading logs, I'd like for my little robot to do something a little different upon each message it receives. In a normal situation, this would require me to logout of the channel, and put the other up. I could potentially miss a nugget from Martin Odersky, which could have horrible consequences. So, the best case scenario would be if I could have my changes without missing a beat.

Now that I got my scenario lined up, I went to look for some java irc client out there. After evaluating several irc libraries out there, I ran across PircBot. It was hosted on maven, which was a win for me and sbt.

I threw together this simple class (and look, it uses my db adapter!), which simply logs into an irc channel, and stores every message into a sqlite database:

import org.jibble.pircbot.PircBot
import hotcode.HotSwap._

class ScalaBot(name: String) extends PircBot {
  this.setName(name)
  this.setLogin("philip")

  val db = Adapter("org.sqlite.JDBC", "jdbc:sqlite:chats.db")

  db.exec("""CREATE TABLE IF NOT EXISTS chats (
              timestamp INTEGER,
              sender STRING,
              msg TEXT)""")(_.execute()) 

  override def onMessage(c: String, sender: String, login: String, h: String, msg: String) {
    db.exec("INSERT INTO chats VALUES (?, ?, ?)") { stm =>
      stm.setLong(1, System.currentTimeMillis)
      stm.setString(2, sender)
      stm.setString(3, msg)
      stm.execute()
    }

    dynamic('message) {
      println("Post hook for dynamic message processing")
    }
  } 
}

Yes! Raw SQL... I'm lovin' it! (Clears throat)

I want to draw your attention to the dynamic call. Notice, right now, it's wrapping a trivial println statement. I'm the kind of programmer, who programs back-doors in every application. Can you Imagine the security risks? I digress...

I launch the application as is, because it solves my first problems. I now want to change it's behavior after it stores a message. Here's where we get to the good stuff.

I used freenode's web app to log in as a human. There's an empty channel (thankfully) at #pircbot where you can test your bots. A playgoround full of robots. I chatted some random things as the user "schmee", and sbt printing out what you'd expect:

[info] Post hook for dynamic message processing
[info] Post hook for dynamic message processing

Great. So at least I know it's running what's in the dynamic block. The following segment is going to change that (fingers crossed).

import hotcode.HotSwap._

object Main extends Application {
  update('message) {
    val db = Adapter("org.sqlite.JDBC", "jdbc:sqlite:chats.db") 
    val message = db.single("SELECT * FROM chats ORDER BY timestamp DESC LIMIT 1")(_.executeQuery())

    println(message)
  }
}

I run that, and chat a couple times more. Low and behold, my sbt output:

[info] Map(timestamp -> 1294867472648, sender -> schmee, msg -> Something special)
[info] Map(timestamp -> 1294867517142, sender -> schmee, msg -> trying it again)

It works, but there are some notable problems. The context is all lost. It works great if I'm reading from a datastore, or changing values in the datastore. I can't really change the bot itself though. I have some ideas about how to address this, but I think the presentation is going to take a hit. At any rate, I was pleased to see it work this well.

No comments:

Post a Comment