Hipbot
Hipbot is a XMPP bot for HipChat, written in Ruby with EventMachine.
Compatibility
Hipbot is tested on:
- Ruby 2.2, 2.3 and 2.4 series
- JRuby (latest)
- Rubinus (latest)
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 = 'changeme@chat.hipchat.com'
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 = 'changeme@chat.hipchat.com'
# 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
# Makes all reactions case insensitive (default: true)
c.case_insensitive = true
# Auto-join on invite (default: true)
c.join_on_invite = true
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 :is_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 loggingadd plugins supportrewrite SimpleMUCClienthandle private messages callbackshandle auto joining on room invite-
add support for custom helpersmentions - returns list of @mentions in messagesender_name - returns sender's first nameallow injecting custom module to response object, adding arbitrary methods
handle reconnecting after disconnect/failureadd support for multiple regexps for one responseadd support for responses in particular room (on //, room: ['public'] do ...
)