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 hipbot1 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 startRun hipbot to see all available commands.
Start in shell:
ruby bot.rbBehavior
- 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
endReaction helpers
Inside the reaction block you have access to following context objects:
botroomsendermessagereaction
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
endconfigure do |c|
# ...
c.join = :none
endconfigure do |c|
# ...
c.join = ['Project Room', :public]
endNotice: 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")
endon /^go away$/ do
bot.set_presence("I'm away", :away)
endon /^do not disturb$/ do
bot.set_presence(nil, :dnd)
endRooms
Use Hipbot::Room for collection of available rooms.
on /^list all rooms$/ do
all_rooms = Hipbot::Room.all.map(&:name)
reply(all_rooms.join(', '))
endon /^get project room JID$/ do
project_room = Hipbot::Room.find_by(name: 'project room')
reply(project_room.id)
endUse 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}"
)
endUsers
Use Hipbot::User for collection of all users:
on /^list all users$/ do
all_users = Hipbot::User.all.map(&:name)
reply(all_users.join(', '))
endon /^get John Smith's JID$/ do
john = Hipbot::Room.find_by(name: 'John Smith')
reply(john.id)
endUse 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}"
)
endUse Room#users method for online users array:
on /^list online users$/ do
reply room.users.map(&:name).join(', ')
endReplying
Use reply method to send a message.
Reply in the same room / chat:
on /^hello$/ do
reply("Hello!")
endReply 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)
endPrivate messaging
on /^send me private message$/ do
sender.send_message("Hello, #{sender}")
endon /^send private message to John$/ do
john = Hipbot::User.find_by(name: 'John Smith')
john.send_message("Hello, John!")
endTopics
on /^current topic$/ do
reply("Current topic: #{room.topic}")
endon /^change topic here$/ do
room.set_topic("New Topic")
endon /^change topic there$/ do
there = Hipbot::Room.find_by(name: 'there')
there.set_topic("New Topic")
endRegexp matchdata
on /^My name is (.*)$/ do |user_name|
reply("Hello, #{user_name}!")
endon /^My name is (\S*) (\S*)$/ do |first_name, last_name|
reply("Hello, #{first_name} #{last_name}!")
endMultiple regexps
on /^My name is (.*)$/, /^I am (.*)$/ do |user_name|
reply("Hello, #{user_name}!")
endSender 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')
endRoom 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!')
endMatch only private messages:
on /^private hello$/, room: false do
reply('Private hello!')
endMatch only room messages:
on /^public hello$/, room: true do
reply('Public hello!')
endGlobal 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...')
endConditional reaction
Use :if option to specify certain dynamic conditions:
on /^Is it friday\?$/, if: ->{ Time.now.friday? } do
reply('Yes, indeed')
endadmins = ['John Smith']
on /^add admin (.*)$/, if: ->(sender){ admins.include?(sender.name) } do |user_name|
admins << user_name
endon /^choose volunteer$/, if: ->(room){ room.users.count > 3 } do
reply("Choosing #{room.users.sample}")
endMethod 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 (.*)$/, :helloPresence 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
endScopes
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
endDefault 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!")
enddefault from: 'Mike Johnson' do
reply("Not you again, Mike!")
endDescriptions
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
endYou can fetch the descriptions and create help reaction, eg:
on /^help$/ do
reply Hipbot.reactions.map(&:desc).compact.join("\n")
endUser 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)
endon /^invite (.*)$/ do |user_name|
user = Hipbot::User.find_by(name: user_name)
room.invite(user)
endHTTP 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
endon /^ping site/ do
get('http://example.com', ping: '1') # GET http://example.com?ping=1
endCustom 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
endPlugins
To define a plugin, include Hipbot::Plugin module in your class:
class GreeterPlugin
include Hipbot::Plugin
on /^hello$/ do
reply('Hello there!')
end
endYou 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
endFor 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
endPreloader 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
endStorage
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
endSample config file:
sessions:
default:
hosts:
- localhost:27017
database: hipbot
options:
allow_dynamic_fields: trueYou 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
endmodule 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
endOther 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
endContributing
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 ...)