Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/pewniak747/hipbot

HipChat bot written in ruby and eventmachine
https://github.com/pewniak747/hipbot

Last synced: 2 months ago
JSON representation

HipChat bot written in ruby and eventmachine

Awesome Lists containing this project

README

        

# Hipbot

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

[![Build Status](https://secure.travis-ci.org/pewniak747/hipbot.png?branch=master)](http://travis-ci.org/pewniak747/hipbot)
[![Code Climate](https://codeclimate.com/github/pewniak747/hipbot.png)](https://codeclimate.com/github/pewniak747/hipbot)
[![Coverage Status](https://coveralls.io/repos/pewniak747/hipbot/badge.png?branch=master)](https://coveralls.io/r/pewniak747/hipbot)
[![Dependency Status](https://gemnasium.com/pewniak747/hipbot.png)](https://gemnasium.com/pewniak747/hipbot)
[![Gem Version](https://badge.fury.io/rb/hipbot.png)](http://badge.fury.io/rb/hipbot)

### 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

```shell
gem install hipbot
```

### 1 minute setup on heroku

Follow the instructions on [hipbot-example](https://github.com/netguru/hipbot-example).

### Custom setup
Create `bot.rb` file, subclass `Hipbot::Bot` and customize the responses.

```ruby
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:

```shell
hipbot start
```

Run `hipbot` to see all available commands.

Start in shell:

```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:
```ruby
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
```
### 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:
```ruby
configure do |c|
# ...
c.join = :private
end
```
```ruby
configure do |c|
# ...
c.join = :none
end
```
```ruby
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:
```ruby
on /^change status$/ do
bot.set_presence("Hello humans")
end
```
```ruby
on /^go away$/ do
bot.set_presence("I'm away", :away)
end
```
```ruby
on /^do not disturb$/ do
bot.set_presence(nil, :dnd)
end
```

### Rooms
Use `Hipbot::Room` for collection of available rooms.
```ruby
on /^list all rooms$/ do
all_rooms = Hipbot::Room.all.map(&:name)
reply(all_rooms.join(', '))
end
```
```ruby
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):
```ruby
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:
```ruby
on /^list all users$/ do
all_users = Hipbot::User.all.map(&:name)
reply(all_users.join(', '))
end
```
```ruby
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:
```ruby
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:
```ruby
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:
```ruby
on /^hello$/ do
reply("Hello!")
end
```
Reply in "help room":
```ruby
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
```ruby
on /^send me private message$/ do
sender.send_message("Hello, #{sender}")
end
```
```ruby
on /^send private message to John$/ do
john = Hipbot::User.find_by(name: 'John Smith')
john.send_message("Hello, John!")
end
```

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

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

### Multiple regexps
```ruby
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.
```ruby
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.
```ruby
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:
```ruby
on /^private hello$/, room: false do
reply('Private hello!')
end
```
Match only room messages:
```ruby
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:

```ruby
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:
```ruby
on /^Is it friday\?$/, if: ->{ Time.now.friday? } do
reply('Yes, indeed')
end
```
```ruby
admins = ['John Smith']
on /^add admin (.*)$/, if: ->(sender){ admins.include?(sender.name) } do |user_name|
admins << user_name
end
```
```ruby
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:
```ruby
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:
```ruby
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:
```ruby
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.
```ruby
default do
reply("I don't understand you!")
end
```
```ruby
default from: 'Mike Johnson' do
reply("Not you again, Mike!")
end
```

### Descriptions
Use `desc` modifier to describe following reaction:
```ruby
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:
```ruby
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.
```ruby
on /^kick (.*)/ do |user_name|
user = Hipbot::User.find_by(name: user_name)
room.kick(user)
end
```
```ruby
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:
```ruby
on /^curl (\S+)$/ do |url|
get(url) do |response|
reply(response.code)
reply(response.headers)
reply(response.body)
end
end
```
```ruby
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:
```ruby
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:
```ruby
class GreeterPlugin
include Hipbot::Plugin

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

You can access plugin data inside reaction with `plugin` helper:
```ruby
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](https://github.com/netguru/hipbot-plugins).

### Exception handling
Define `on_exception` block in your Hipbot class to handle runtime exceptions:
```ruby
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:
```ruby
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:
```ruby
require 'hipbot/storages/mongoid'
configure do |c|
# ...
c.storage = Hipbot::Storages::Mongoid
end
```
Sample config file:
```yaml
sessions:
default:
hosts:
- localhost:27017
database: hipbot
options:
allow_dynamic_fields: true
```
You can optionally override user and room classes with these base models:
```ruby
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
```
```ruby
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](https://github.com/pewniak747/hipbot/blob/master/lib/hipbot/storages/base.rb)
```ruby
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 ...`)~~

Read [the story behind creating HipBot](https://netguru.co/blog/posts/hipbot).