Hipbot

Hipbot is a XMPP bot for HipChat, written in Ruby with EventMachine.

Build Status Code Climate Coverage Status Dependency Status Gem Version

Compatibility

Hipbot is tested on:

  • Ruby 1.9.2, 1.9.3 and 2.0.0
  • JRuby 1.9 mode
  • Rubinus 1.9 mode

Dependencies

  • daemons >= 1.1.8
  • activesupport >= 3.2.12
  • eventmachine >= 1.0.3
  • em-http-request >= 1.0.3
  • xmpp4r ~> 0.5

Getting started

Installation

gem install hipbot

1 minute setup on heroku

Follow the instructions on hipbot-example.

Custom setup

Create bot.rb file, subclass Hipbot::Bot and customize the responses.

require 'hipbot'

class MyBot < Hipbot::Bot
  configure do |c|
    c.jid       = '[email protected]'
    c.password  = 'secret'
  end

  on /^hello$/ do
    reply("Hello!")
  end
end

MyBot.start!

Running

Start Hipbot as a daemon by executing:

hipbot start

Run hipbot to see all available commands.

Start in shell:

ruby bot.rb

Behavior

  • On start and runtime:
    • Fetches details and presences of all users in Lobby
    • Pings XMPP server every 60 seconds to keep alive
  • On new message:
    • Invokes all matching reactions or falls back to default reaction

Usage

Configuration

Full configuration example:

class MyBot < Hipbot::Bot
  configure do |c|
    # Account JID (required) - see https://hipchat.com/account/xmpp for your JID
    c.jid      = '[email protected]'

    # Account password (required)
    c.password = 'secret'

    # Custom helpers module (optional) - see below for examples
    c.helpers  = MyHipbotHelpers

    # Logger (default: Hipbot::Logger.new($stdout))
    c.logger   = Hipbot::Logger.new($stdout)

    # Initial status message (default: '')
    c.status   = "I'm here to help"

    # Storage adapter (default: Hipbot::Storages::Hash)
    c.storage  = Hipbot::Storages::Hash

    # Predefined room groups (optional)
    c.rooms    = { project_rooms: ['Project 1', 'Project 2'] }

    # Predefined user groups (optional)
    c.teams    = { admins: ['John Smith'] }

    # Auto join criteria (default: :all)
    # Accepted values: :all, :public, :private, :none, "room name"
    c.join     = :private
  end
end

Reaction helpers

Inside the reaction block you have access to following context objects:

  • bot
  • room
  • sender
  • message
  • reaction

Joining rooms

Hipbot will join all accessible rooms by default on startup and invite.

To change auto join method use join configuration option:

configure do |c|
  # ...
  c.join = :private
end
configure do |c|
  # ...
  c.join = :none
end
configure do |c|
  # ...
  c.join = ['Project Room', :public]
end

Notice: Archived rooms are always ignored

Bot presence

Use bot.set_presence method to change Hipbot presence:

on /^change status$/ do
  bot.set_presence("Hello humans")
end
on /^go away$/ do
  bot.set_presence("I'm away", :away)
end
on /^do not disturb$/ do
  bot.set_presence(nil, :dnd)
end

Rooms

Use Hipbot::Room for collection of available rooms.

on /^list all rooms$/ do
  all_rooms = Hipbot::Room.all.map(&:name)
  reply(all_rooms.join(', '))
end
on /^get project room JID$/ do
  project_room = Hipbot::Room.find_by(name: 'project room')
  reply(project_room.id)
end

Use room for current room object (it's nil if message is private):

on /^where am I\?$/ do
  reply(
    "You are in #{room}\n" +
    "JID: #{room.id}\n" +
    "Topic: #{room.topic}\n" +
    "Users online: #{room.users.count}\n" +
    "Privacy: #{room.privacy}\n" +
    "Hipchat ID: #{room.hipchat_id}\n" +
    "Archived?: #{room.archived ? 'yes' : 'no'}\n" +
    "Guest URL: #{room.guest_url}"
  )
end

Users

Use Hipbot::User for collection of all users:

on /^list all users$/ do
  all_users = Hipbot::User.all.map(&:name)
  reply(all_users.join(', '))
end
on /^get John Smith's JID$/ do
  john = Hipbot::Room.find_by(name: 'John Smith')
  reply(john.id)
end

Use sender for message sender object:

on /^who am I\?$/ do
  reply(
    "You are #{sender}\n" +
    "JID: #{sender.id}\n" +
    "Mention: @#{sender.mention}\n" +
    "E-mail: #{sender.email}\n" +
    "Title: #{sender.title}\n" +
    "Photo: #{sender.photo}"
  )
end

Use Room#users method for online users array:

on /^list online users$/ do
  reply room.users.map(&:name).join(', ')
end

Replying

Use reply method to send a message.

Reply in the same room / chat:

on /^hello$/ do
  reply("Hello!")
end

Reply in "help room":

on /^I need help$/ do
  help_room = Hipbot::Room.find_by(name: 'help room')
  reply("#{sender} needs help in #{room}", help_room)
end

Private messaging

on /^send me private message$/ do
  sender.send_message("Hello, #{sender}")
end
on /^send private message to John$/ do
  john = Hipbot::User.find_by(name: 'John Smith')
  john.send_message("Hello, John!")
end

Topics

on /^current topic$/ do
  reply("Current topic: #{room.topic}")
end
on /^change topic here$/ do
  room.set_topic("New Topic")
end
on /^change topic there$/ do
  there = Hipbot::Room.find_by(name: 'there')
  there.set_topic("New Topic")
end

Regexp matchdata

on /^My name is (.*)$/ do |user_name|
  reply("Hello, #{user_name}!")
end
on /^My name is (\S*) (\S*)$/ do |first_name, last_name|
  reply("Hello, #{first_name} #{last_name}!")
end

Multiple regexps

on /^My name is (.*)$/, /^I am (.*)$/ do |user_name|
  reply("Hello, #{user_name}!")
end

Sender restriction

Use :from option to match messages only from certain users or user groups defined in configuration. It accepts string, symbol and array values.

configure do |c|
  # ...
  c.teams = { vip: ['John Edward', 'Mike Anderson'] }
end

on /^report status$/, from: ['Tom Smith', 'Jane Doe', :vip] do
  reply('All clear')
end

Room restriction

Use :room option to match messages opny from certain HipChat rooms. It accepts string, symbol, array and boolean values.

configure do |c|
  # ...
  c.rooms = { project_rooms: ['Project 1', 'Project 2'] }
end

on /^hello$/, room: ['Public Room', :project_rooms] do
  reply('Hello!')
end

Match only private messages:

on /^private hello$/, room: false do
  reply('Private hello!')
end

Match only room messages:

on /^public hello$/, room: true do
  reply('Public hello!')
end

Global reaction

By default, Hipbot reacts only to its HipChat mention. Use global: true option to match all messages:

on /^Hey I just met you$/, global: true do
  reply('and this is crazy...')
end

Conditional reaction

Use :if option to specify certain dynamic conditions:

on /^Is it friday\?$/, if: ->{ Time.now.friday? } do
  reply('Yes, indeed')
end
admins = ['John Smith']
on /^add admin (.*)$/, if: ->(sender){ admins.include?(sender.name) } do |user_name|
  admins << user_name
end
on /^choose volunteer$/, if: ->(room){ room.users.count > 3 } do
  reply("Choosing #{room.users.sample}")
end

Method reaction

Use symbol instead of block to react with a instance method:

def hello(user_name)
  reply("Hello #{user_name}!")
end

on /^My name is (.*)$/, :hello

Presence reaction

Use on_presence in the same way as on to make presence reactions:

class MyBot < Hipbot::Bot
  # ...
  on_presence do |status|
    case status
    when 'unavailable'
      reply("Bye bye, #{sender.name}!")
    when ''
      reply("Welcome, #{sender.name}!")
    end
  end
end

Scopes

Use scope blocks to extract common options:

configure do |c|
  # ...
  c.teams = { admins: ['John Edward', 'Mike Anderson'] }
end

scope from: :admins, room: true do
  on /^restart server$/ do
    # Restarting...
  end

  scope global: true do
    on /^deploy production$/ do
      # Deploying...
    end

    on /^check status$/ do
      # Checking...
    end
  end
end

Default reactions

Default reaction can take the same options as regular one. Hipbot fall backs to default reactions if there is no matching normal reaction.

default do
  reply("I don't understand you!")
end
default from: 'Mike Johnson' do
  reply("Not you again, Mike!")
end

Descriptions

Use desc modifier to describe following reaction:

desc '@hipbot restart server_name - Restarts the server'
on /^restart (.*)$/ do |server|
  if server.empty?
    reply("Usage: #{reaction.desc}")
  else
    # Restarting...
  end
end

You can fetch the descriptions and create help reaction, eg:

on /^help$/ do
  reply Hipbot.reactions.map(&:desc).compact.join("\n")
end

User managment

This behavior is experimental and not officially supported by HipChat. Bot must be an admin in order to perform these actions.

on /^kick (.*)/ do |user_name|
  user = Hipbot::User.find_by(name: user_name)
  room.kick(user)
end
on /^invite (.*)$/ do |user_name|
  user = Hipbot::User.find_by(name: user_name)
  room.invite(user)
end

HTTP helpers

Use get, post, put and delete helpers to preform a HTTP requests:

on /^curl (\S+)$/ do |url|
  get(url) do |response|
    reply(response.code)
    reply(response.headers)
    reply(response.body)
  end
end
on /^ping site/ do
  get('http://example.com', ping: '1') # GET http://example.com?ping=1
end

Custom response helpers

You can define your own helpers and use them inside responses like this:

module MyHipbotHelpers
  def project_name
    "#{room.name}-project"
  end
end

class Bot < Hipbot::Bot
  configure do |c|
    # ...
    c.helpers = MyHipbotHelpers
  end

  on /^what's the project name\?$/ do
    reply(project_name)
  end
end

Plugins

To define a plugin, include Hipbot::Plugin module in your class:

class GreeterPlugin
  include Hipbot::Plugin

  on /^hello$/ do
    reply('Hello there!')
  end
end

You can access plugin data inside reaction with plugin helper:

class GreeterPlugin
  include Hipbot::Plugin

  attr_accessor :language

  on /^hello$/ do
    case plugin.language
    when :en
      reply("Hello!")
    when :pl
      reply("Cześć!")
    when :jp
      reply("おはよう!")
    end
  end
end

GreeterPlugin.configure do |c|
  c.language = :jp
end

For more examples, check out hipbot-plugins.

Exception handling

Define on_exception block in your Hipbot class to handle runtime exceptions:

class MyBot < Hipbot::Bot
  on_exception do |e|
    hipbot_room = Hipbot::Room.find_by(name: 'hipbot room')
    reply(e.message, hipbot_room)
    # If exception was raised in reaction, there are some context variables available:
    reply("#{e.message} raised by #{message.body} from #{sender} in #{room}", hipbot_room)
  end
end

Preloader for EventMachine

In order to use EventMachine runtime methods, define them within on_preload block in your Hipbot class:

class MyBot < Hipbot::Bot
  on_preload do
    EM::add_periodic_timer(60) do
      Updater::update_stock_prices
      Updater::update_server_statuses
    end
  end
end

Storage

Hipbot uses in-memory hash storage by default, however you can use persistent storage adapter to speed up boot time and extend the functionality.

MongoDB

In order to use MongoDB storage, enable Mongoid adapter and add allow_dynamic_fields: true to your Mongoid config:

require 'hipbot/storages/mongoid'
configure do |c|
  # ...
  c.storage = Hipbot::Storages::Mongoid
end

Sample config file:

sessions:
  default:
    hosts:
      - localhost:27017
    database: hipbot
options:
  allow_dynamic_fields: true

You can optionally override user and room classes with these base models:

module Hipbot
  class User
    include Mongoid::Document

    has_and_belongs_to_many :rooms, class_name: 'Hipbot::User', inverse_of: :users

    field :email,      type: String
    field :mention,    type: String
    field :phone,      type: String
    field :photo,      type: String
    field :title,      type: String
    field :is_online,  type: Boolean
  end
end
module Hipbot
  class Room
    include Mongoid::Document

    has_and_belongs_to_many :users, class_name: 'Hipbot::User', inverse_of: :rooms

    field :archived,   type: Boolean
    field :guest_url,  type: String
    field :hipchat_id, type: String
    field :privacy,    type: String
    field :topic,      type: String
  end
end

Other storage

Storage adapter is included in room and user classes upon loading. Make sure your adapter implements all methods from Hipbot::Storages::Base

module MyStorageAdapter
  include Hipbot::Storages::Base
  # ...
end

configure do |c|
  # ...
  c.storage = MyStorageAdapter
end

Contributing

To do:

  • add tests for Match class
  • add testing adapter for testing custom responses with RSpec
  • add HipChat API integration (?)

Done:

  • ~~add extended logging~~
  • ~~add plugins support~~
  • ~~rewrite SimpleMUCClient~~
  • ~~handle private messages callbacks~~
  • ~~handle auto joining on room invite~~
  • ~~add support for custom helpers~~
    • ~~mentions - returns list of @mentions in message~~
    • ~~sender_name - returns sender's first name~~
    • ~~allow injecting custom module to response object, adding arbitrary methods~~
  • ~~handle reconnecting after disconnect/failure~~
  • ~~add support for multiple regexps for one response~~
  • ~~add support for responses in particular room (on //, room: ['public'] do ...)~~