Ruby
Article

Fun with Robots, Lita, and HipChat

By Ilya Bodrov-Krukowski

lita

I bet you’ve watched some kind of a science fiction film where a hero boots up a super-duper PC and, instead of doing all the tasks manually with a keyboard and mouse, just uses voice commands (while drinking coffee or cleaning the superhero costume). That looks really cool. Couldn’t we create something similar, yet simpler, with Ruby?

For example, have some kind of a robot that will respond to our commands that we (for now :)) type with the keyboard. Also, it would be nice if new commands could be added to extend our robot’s functionality. This can be done fairly easy!

Meet Lita, a cute robot companion for your chat room. It can connect to any chat service and easily be extended with custom plugins.

In this article, I am going to show you how to setup Lita, connect it to HipChat (a popular chatting platform) and enhance our chat bot with new commands to interact with an API of a sample Rails application.

Source code for the sample todo app is available at github.com/bodrovis/SitePointReminder.

Source code for the bot is available at github.com/bodrovis/SitePointBot.

Lita’s Features

Okay, what goodies can Lita offer us? As I said, it can work with any chatting service. Currently, IRC, HipChat,
Campfire, Slack, Twitter, and Japaneese Idobata are supported, but you can write your own adapter to connect to any platform that you need.

There are variety of plugins already created that provide different functionality (like fetching whois information, querying Google, interacting with JIRA, and many others) and you can easily write your own.

With Lita you can automate tedious tasks and perform them by simply typing “Lita, please do that” in the chat room. Of course, the project is open source and written in Ruby, the best programming language in the world :).

I hope that you are eager to start tinkering with Lita right away!

Connecting with HipChat

For this demo I’ve decided to use HipChat, so you will need to visit hipchat.com and create two accounts: one for you and one for your faithful robot. Confirm and put them aside for now.

Next, install lita itself by running:

$ gem install lita

If you are on Windows, then errors related to Puma installation will appear, as this web server is one of the Lita’s dependencies. To overcome this problem refer to this guide. In a nutshell, you will need to download and extract this OpenSSL package. Then, copy libeay32.dll and ssleay32.dll to your ruby/bin directory and run the following command:

$ gem install puma -- --with-opt-dir=c:\openssl

Replacing c:\openssl with the path where OpenSSL was extracted. When you are done the new lita command should be available from your command line.

Before creating a new robot, the Redis DBMS should be also installed, as Lita uses it as data storage. Visit the redis.io/download page to choose the version that suits you (an unofficial Windows version is available as well). After starting up Redis, you are ready to proceed!

Type the following command to create a new Lita project (let’s call it Bot):

$ lita new bot

A bot directory and a couple of files will be created. First of all edit the Gemfile by uncommenting the following line:

Gemfile

[...]
# gem "lita-hipchat"
[...]

Next run

$ bundle install

The HipChat adapter is now in place. Next open up lita_config.rb, which contains all the necessary settings. Change these parameters:

  • config.robot.name (Which you provided when creating the bot’s account on HipChat)
  • config.robot.adapter (Set it to :hipchat)
  • config.redis (Pass a hash here, like: {host: "127.0.0.1", port: 6379})
  • config.adapters.hipchat.jid (To find it visit hipchat.com, open Account Settings, and navigate to the XMPP/Jabber info page. The “Jabber ID” field is what you need)
  • config.adapters.hipchat.password (The bot’s password)
  • config.adapters.hipchat.debug (Set this to true to view debugging information in the console. It is useful if your bot cannot connect to the chatting platform, for example)
  • config.adapters.hipchat.rooms (Chat room name or :all)

Great! After changing those settings you are ready to connect. Type

$ lita

in your console (from the directory where the bot’s files reside) to boot the application up. Next, go to hipchat.com and log in with your personal account. Join the chat room, your bot will already be there awaiting your orders! Of course, at this point, it is not very useful. You can check Lita’s version by typing:

@lita info
# or
lita, info
# or
lita: info
# or
lita info

Don’t forget to replace lita with your bot’s name. By the way, if you don’t want to always specify your bot’s name when issuing commands, set the config.adapters.shell.private_chat option to true. This treats all messages as commands, so you won’t need to prefix them with Lita’s name.

If this command succeeds then everything is working.

Integrating Lita Plugin

Your bot can respond to a very limited range of commands (like showing the version or joining a room – read more about them here). Luckily, there are already a wide variety of plugins available for Lita. They are divided into three types:

  • Adapters are used to connect to a specific chat service (like lita-hipchat).
  • Handlers add new functionality that users will interface with at runtime. Specifically, they can work with chat and HTTP routes. We will discuss chat routes later.
  • Extensions provide new features for developing other plugins and extending the core Lita framework. They are used in complex scenarios so, in many cases, you won’t need to employ them.

Let’s try to integrate a couple of handlers into our project. For example, there is a lita-google-images handler that queries Google Images with specified keywords and returns one of the relevant images as a result. This plugin is really easy to integrate. Just add it to your Gemfile:

Gemfile

[...]
gem "lita-google-images"
[...]

run

$ bundle install

and restart your bot. Now you may issue commands like:

@lita image cat
@lita img cat

Cool!

Another potentially useful and yet very easy to integrate extension is lita-whois which searches WHOIS records (with the help of Whois client). Once again just drop this line to your Gemfile:

Gemfile

[...]
gem "lita-whois"
[...]

run

$ bundle install

and restart Lita. Now you may issue commands like:

@lita whois example.com
@lita whois 8.8.8.8

to view the full information.

If you wish to record all the chatting activity somewhere on your local server (to find out what your colleagues are saying about you when you’re not online :)) then grab lita-logger. This one requires some basic setup via the lita_config.rb file:

lita_config.rb

[...]
config.handlers.logger.log_file = "chat.log" # replace this with a path to your log file
config.handlers.logger.enable_http_log = true
[...]

Don’t forget to run

$ bundle install

and restart your app. Now all activity in the chat will be logged to the specified file. Big Brother (or Sister?) is watching you, eh?

As homework, integrate the lita-chuck_norris extension to read some jokes about this tough guy when you feel bored (they are fetched by issuing a GET request to http://api.icndb.com/jokes/random.json). By the way, there are many other similar plugins created just for fun, so check out the plugins page. Maybe you will be inspired to create something on your own!

Creating (Yet Another) Todo App

C’mon, don’t give me that look. You are probably sick of Todo apps, but in this demo it will perform solely a supporting role. We need an app for our bot to interact with it via an API.

This app will support the following:

  • Create a todo
  • Edit a todo
  • List todos
  • Mark a todo as done (which essentially means destroying it)

All those actions will be accessible via a simple API, which our bot is going to consume. No authentication, authorization, or that kind of stuff.

This app can actually be written in any language/framework, but I am going to stick with Rails 4.1. Create a new app called Reminder without the default testing suite:

$ rails new reminder -T

You may style it a bit or leave it with the default ugly looking theme, as the style has nothing to do with our goal. If you wish to follow along, then add this gem to the Gemfile:

[...]
gem 'bootstrap-sass'
[...]

Also, make sure than the gem 'jbuilder' is uncommented, because we will need it to return results via the API.

Then run

$ bundle install

Drop these lines to the application.css.scss file

application.css.scss

[...]
@import 'bootstrap';
@import 'bootstrap/theme';
[...]

and tweak the layout to take advantage of Bootstrap’s styles, if you want to:

layouts/application.html.erb

[...]
<div class="container">
  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>

  <div class="page-header">
    <h1><%= yield :page_header %></h1>
  </div>

  <%= yield %>
</div>
[...]

Now, let’s generate a dead simple Todo model and apply the corresponding migration:

$ rails g model Todo title
$ rake db:migrate

Add some necessary routes:

config/routes.rb

[...]
resources :todos, only: [:new, :create, :index, :destroy]
root to: 'todos#index'
[...]

On to the controller. Let’s start with the index action:

todos_controller.rb

class TodosController < ApplicationController
  def index
    respond_to do |format|
      @todos = Todo.order('created_at DESC')
      format.html
      format.json
    end
  end
end

We’ve decided to build a simple API, so utilize the respond_to method to present the user with the required format. Corresponding views should also be created. First, the HTML format:

todos/index.html.erb

<% content_for(:page_header) { "Your todos" } %>

<%= link_to 'Add todo', new_todo_path, class: 'btn btn-primary' %>

<div class="panel">
  <div class="panel-body">
    <ul>
      <% @todos.each do |todo| %>
        <li><%= todo.title %> <%= link_to 'Done', todo_path(todo), method: :delete %></li>
      <% end %>
    </ul>
  </div>
</div>

And the JSON format:

todos/index.json.jbuilder

json.array! @todos.each do |todo|
  json.id todo.id
  json.title todo.title
end

We are rendering an array of todos; each one has an id and a title. Don’t forget that you will need jBuilder hooked up in the project for this to work correctly.

OK, now the new and create actions:

todos_controller.rb

class TodosController < ApplicationController
  [...]
  def new
    @todo = Todo.new
  end

  def create
    @todo = Todo.new(todo_params)
    respond_to do |format|
      if @todo.save
        format.html do
          flash[:success] = 'Todo created!'
          redirect_to root_path
        end
        format.json { head :no_content }
      else
        format.html { render :new }
        format.json { render json: @todo.errors.full_messages, status: :unprocessable_entity }
      end
    end
  end
  [...]

  private

  def todo_params
    params.require(:todo).permit(:title)
  end
end

Once again, we are using respond_to. Note that the new action does not need the corresponding .json view because the response it being rendered right in the controller.

Lastly, the destroy action:

todos_controller.rb

def destroy
  todo = Todo.find_by_id(params[:id])
  respond_to do |format|
    if todo && todo.destroy
      format.html do
        flash[:success] = 'Todo marked as done!'
        redirect_to root_path
      end
      format.json { head :no_content }
    else
      format.html do
        flash[:warning] = 'There was an error.'
        redirect_to root_path
      end
      format.json { head 500 }
    end
  end

One more thing: tweak the ApplicationController so an exception is not raised when the CSRF token is not provided (when creating the record for example). In a real app, you would set up some kind of authentication to keep users from doing something they are not supposed to do, but we’re hanging loose today:

application_controller.rb

[...]
protect_from_forgery
[...]

I’ve just removed with: exception here so that :null_session is used (it is the default option). By the way, above this line there is a tip:

# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.

so that you don’t forget about this. Read more about this here.

Brilliant! Yet another todo app is created and we can teach our robot how to work with it.

Creating a Custom Lita Handler

Remember, there are three types of plugins for Lita: adapters, handlers, and extensions. In order to add custom functionality, build an extension.

If you are planning to share your new shiny plugin with the world, then use one of these three commands:

  • lita adapter NAME
  • lita handler NAME
  • lita extension NAME

This will set up the basic file structure for you and prefix the name of the plugin with lita- (this is a convention that Lita’s author introduced). Later, you can package your code as a gem, publish it to RubyGems, and start receiving rhapsodic feedback from the users.

However, our handler is for internal use only, so let’s keep things simple. Go ahead and create a new file lita-reminder.rb in your bot directory which currently contains the lita_config.rb and Gemfile files.

Modify the config file by adding the following line to the top:

lita_config.rb

require './lita-reminder'
[...]

Now Lita is aware of our new, yet empty, handler. Paste the following code into lita-reminder.rb:

lita-reminder.rb

module Lita
  module Handlers
    class Reminder < Handler
    end
  end
  Lita.register_handler(Reminder)
end

The handler will now be registered when the robot boots up. It is time to flesh it out. Let’s implement three actions:

  • add todo
  • destroy (mark as done) todo
  • list todos

A chat route will be required to tell our robot when to perform which action. Basically, a chat route is a regular expression. If Lita finds a match in one of the messages, it performs the corresponding operation. Let’s create one:

lita-reminder.rb

[...]
class Reminder < Handler
  route(/^remind plz$/, :index, command: true, help: { "remind plz" => "Remind about your todos." })
end
[...]

When Lita sees a message that starts with “remind plz”, it will call an index method, which will be created shortly. The command: true here means that this route will only be triggered if the bot is mentioned in the message (the message is either private or prefixed with bot’s name). help contains the sample invocation of the route and what it does. In our case, type

@lita help remind plz

to see this help message. Note that the response with the help will always be sent as a private message – not sure if this is a bug or expected behavior.

We can now implement the index method and some utility methods:

lita-reminder.rb

[...]
def index(response)
  todos = parse get("http://127.0.0.1:3000/todos.json")
  if todos.any?
    response.reply("Your todos:")
    todos.each do |todo|
      response.reply("# #{todo['id']}: #{todo['title']}")
    end
  else
    response.reply('You have done all the todos! Good job!')
  end
end

private

def get(url)
  Net::HTTP.get make_uri(url)
end

def parse(obj)
  MultiJson.load(obj)
end

def make_uri(url)
  URI(url)
end

[...]

Those utility methods are simple: get is used to send GET request to the provided URI, parse parses a JSON response (MultiJson is hooked up in the Lita core) and make_uri creates a URI from the provided URL.

What is the response parameter passed to the index method? It is an instance of the Lita::Response class and is the primary interface for inspecting details about the message and responding to it. It has a couple of useful methods. Here, we are using only one – reply, which replies back with the provided string.

Basically, the index method sends a GET request to http://127.0.0.1:3000/todos.json, parses the response, and either renders a list of todos in the ID - title format or says that there are no todos found.

What if, in the future, our todos app is moved to the production server? We would need to alter the link to it everywhere it is used. Couldn’t we just create some kind of a setting so that the user can provide a link to the todo app there? It appears we can!

lita-reminder.rb

[...]
config :server

[...]

def index(response)
  todos = parse get("#{config.server}/todos.json")
  [...]
end

[...]

The config :server here means that the server is an option available for users to specify. It can then be accessed by using config.server (by the way all the config variables are frozen at runtime). Provide this new setting:

lita_config.rb

[...]
Lita.configure do |config|
    [...]
    config.handlers.reminder.server = 'http://127.0.0.1:3000'
end

Nice! Now, add the route for adding a new todo:

lita-reminder.rb

[...]

module Lita
  module Handlers
    class Reminder < Handler
      route(/^todo\s+(.+)$/, :create, command: true, help: { "todo TODO" => "Adds new todo to the list." })

      def create(response)
        todo = response.match_data[1]
        result = post "#{config.server}/todos.json", 'todo[title]' => todo
        if result.code.to_i.success?
          response.reply("#{todo} was added.")
        else
          response.reply("I've encountered the following errors while saving your todo: #{parse(result.body)}")
        end
      end

      private

      def post(url, data = {})
        Net::HTTP.post_form(make_uri(url), data)
      end
    end
    Lita.register_handler(Reminder)
  end
end

class Numeric
  def success?
    self > 199 && self < 300
  end
end

In the route, I am using a capturing group to get the name of the new todo. The create method uses response.match_data[1] to fetch this capturing group in the match data (match_data is yet another method available from the response). The rest of the code is pretty simple: we issue a POST request providing the todo’s title.

Finally, the destroy route:

lita-reminder.rb

[...]
route(/^done todo\s+(\d+)$/, :destroy, command: true, help: { "done todo TODO_ID" => "Marks todo with the specified number as done." })
[...]
def destroy(response)
  todo_id = response.match_data[1]
  result = delete "#{config.server}/todos/#{todo_id}.json"
  if result.code.to_i.success?
    response.reply("Todo # #{todo_id} was marked as done.")
  else
    response.reply("I've encountered an error while marking your todo as done.")
  end
end

private

def delete(url)
  uri = make_uri(url)
  http = Net::HTTP.new(uri.host, uri.port)
  request = Net::HTTP::Delete.new(uri.path)
  http.request(request)
end
[...]

The ID of the todo to destroy should be provided. Later, the todo is fetched using the match_data[1] construction. Then, a DELETE request is sent.

By the way, if you don’t want your todos to be destroyed completely then acts_as_paranoid or a similar solution may be implemented.

Boot your robot and try to manage your todos. Wouldn’t it be cool if Lita could also cook a dinner for you?

Conclusion

In this article, we had a look at Lita, an extensible robot companion. We’ve integrated it with HitChat, added a bunch of plugins, and even wrote our own handle to communicate with a todo app API. Feel free to experiment with this code or create something more complex (don’t forget to share a link in the comments!). Also, please share your experience if you have ever used Lita in production to perform some real tasks – that would be interesting!

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

Comments
Suresh_Kumar

Hi Ilya Bodrov

Thanks for the post.
Could you please share how the end result would be?
For example, the app hosted on heroku would be nice.

Thanks.
Your fan.

bodrovis

Hello, Suresh!

The problem here is that you will have to start up the bot on your local machine anyways. Of course I could set up demo for the todo app if you wish so that you can issue API requests.

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in Ruby, once a week, for free.