Hipbot is a XMPP bot for HipChat, written in Ruby with EventMachine.
Hipbot is tested on:
- Ruby 2.2, 2.3 and 2.4 series
- JRuby (latest)
- Rubinus (latest)
- daemons >= 1.1.8
- activesupport >= 3.2.12
- eventmachine >= 1.0.3
- em-http-request >= 1.0.3
- xmpp4r ~> 0.5
gem install hipbot
Follow the instructions on hipbot-example.
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!
Start Hipbot as a daemon by executing:
hipbot start
Run hipbot
to see all available commands.
Start in shell:
ruby bot.rb
- 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
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
# Makes all reactions case insensitive (default: true)
c.case_insensitive = true
# Auto-join on invite (default: true)
c.join_on_invite = true
end
end
Inside the reaction block you have access to following context objects:
bot
room
sender
message
reaction
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
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
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
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
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
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
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
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
on /^My name is (.*)$/, /^I am (.*)$/ do |user_name|
reply("Hello, #{user_name}!")
end
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
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
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
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
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
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
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 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
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
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
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
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
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.
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
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
Hipbot uses in-memory hash storage by default, however you can use persistent storage adapter to speed up boot time and extend the functionality.
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
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
- add tests for Match class
- add testing adapter for testing custom responses with RSpec
- add HipChat API integration (?)
add extended loggingadd plugins supportrewrite SimpleMUCClienthandle private messages callbackshandle auto joining on room inviteadd 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 ...
)