Boty

Boty is a utilitary to create bots (at this time, specificaly Slack bots).

A bot in the context of this gem is an ordinary ruby application that knows how to connect and receive messages in a Slack company room. Boty will give you a nice api to bind your own logic to specific messages of your interest.

Usage TL;DR version:

  • create new slack bot integration (http://[company].slack.com/services)
  • $ gem install boty
  • $ boty new jeeba (where jeeba it's a name that you will choose for the bot in the integration step)
    • type your company name (the one used in the slack url: http://[company].slack.com)
    • type the api key created on the bot integration step
  • $ cd jeeba (the dir created after the name passed as parameter to the new command
  • ./bot

Your bot should be up and running, which means: connected to your Slack room under the name configured in the Slack Bot integration.

Custom scripts

On the script dir of your bot there is a example ping script. But you will want to create your own scripts:

  • stop the bot (if it's already running)
  • add a new file named hello.rb into the script dir
  • add a command to made it available via bot mention (@jeeba in my case)
  • add a hear instruction so the bot listen to any message with the pattern configured
    • use the method #say to instruct the bot to send a message in the channel
    • use the method #im to instruct the bot to send a private message
# script/hello.rb
command "hi" do
  say "hi #{user.name}"
end

command "im me" do
  im "want something?"
end

hear "say something" do
  say "something"
end
  • run the bot ./bot
  • on the slack room, issue the commands and send messages:
valeriano 12:14 AM
@jeeba: hi

jeeba BOT 12:14 AM
hi valeriano

valeriano 12:14 AM
@jeeba: im me

# a private message will arrive:
jeeba BOT 12:14 AM
want something?

# back to the general channel:
valeriano 12:16 AM
say something

jeeba BOT 12:16 AM
something

Regexes

There is an api to capture matches when regexes are used on hear and command configurations:

  • stop the bot (if it's already running)
  • add a new file named math.rb into the script dir
  • add the command /sum (\d+) (\d+)/:
# script/math.rb
command(/sum (\d+) (\d+)/) do |augend, addend|
  say "(#{augend} plus #{addend}) = #{augend.to_i + addend.to_i}"
end
  • run the bot: ./bot
  • on the slack room, issue the command:
valeriano 12:30 AM
@jabberu: sum 100 200

jabberu BOT 12:30 AM
(100 plus 200) = 300

And this is pretty much it! :tada:

If you need more details about any of the features in Boty, you probably will find it in this README, on the sections that follow. If you don't find what you need, please contact us or even create an issue. Our goals with this project are to make it usefull and easy to use, check the contributing info for more.

Usage (long version)

Now you will learn how to create a custom bot application for your needs, how to configure it to allow the bot say stuff in your slack channels and finally, will see how to run the bot properly.

Slack Bot Integration

The first thing to do is to create a Slack Bot integration. Go to http://[your-company-name].slack.com/services to add a new one. In the process an API Token will be generated. That will be used in the next step.

Installation

Now you can install Boty:

$ gem install boty

This will give you an executable boty, which ships with a command help so you can know all the stuff that it can do. But the main one is to create your new shiny bot.

Creating a new bot

To create a new bot, execute this on your terminal:

$ boty new jeeba

Where jeeba will be the nickname you give your bot in the Slack integration.

The command will create a new directory after your bot name (jeeba in my example). Your bot application will live in this directory. Feel free to check the contents, it will have some ruby files, some directories... it's just an ordinary ruby application.

But first, let's see something about the configurations.

Configuration

When executing the boty new command, you will be prompted for the company name and the bot api key.

The information that you enter will be used only locally, so you can experiment and test (we will see more about the automated ones in this README) while developing your bot. This will be stored in the .env.local file on the recently created dir.

If you want to understand how this configuration is managed locally, you can check de Dotenv gem documentation, but you don't need to worry about it, if you don't want.

Now, let's create some commands and message listeners on your bot.

Creating a Session (and the bot file).

Since you already have a Boty project configured with your company name and api key, you should be able to start a new session for your bot by calling Session#start.

This is a "blocking" call. It will hang to allow the bot to listen for messages and send them. You can pass a block to it in order to configure your bot preferences, or knowledge if you prefer =).

The block passed to #start will be yielded in the scope of a Bot. In other words, you can call any bot method inside this context. Note that at this time, you can consider your Bot already connected to the Slack room.

session = Boty::Session.new
session.start do
  # configure your bot here
end

In your project there is a bot executable file. This file already creates a Session with a minimum bot configuration: a listener with the bot name. Which means that any message with the bot name will trigger the behavior in this configuration.

This is just to save you time. You could start a Session by hand, if you wanted, so feel free to check the code on this file and change it at will =).

Now it's time to better understand what is a listener, a command and how to use regexes to configure the patterns that trigger the bot.

Command - a bot mention followed by a instruction pattern

A command can be understood as a direct instruction to the bot. To be issued the bot should be mentioned by the message using the default Slack mention "operator" @.

To create a command, call the method #command on the bot passed as parameter in the Session#start method. The command method receive the pattern for the command, and the block that should be executed when the command is issued.

See how to teach your bot to flip tables like a pro.

session = Boty::Session.new
session.start do
  # ...

  command("flip") do
    say "HEY! (╯°□°)╯︵ ┻━┻"
  end
end

Now, to see the result, with the bot running, send in any channel where the bot is (the #general is the default):

@jeeba: flip

(remember: jeeba is the name that I choose for my bot, you should use the correct name here).

The previous command will result in the following response:

jeeba BOT 2:25 AM
HEY! (╯°□°)╯︵ ┻━┻

There is an alternative method to create a command: bot#response. Use the one more appealing to you, they are the same.

The bot#say method used in this example is better explained here.

Wrapping up:

  • a command is a pattern triggered by a bot mention
  • you can configure your bot patterns in the block passed to Session#start

Now, let's see how a listener works.

Listener - a message pattern that the bot should respond to

A listener is used when the bot is interested in ANY messages containing some pattern. Note that a listener will not be triggered only when there is a bot mention, but anytime a message containing the pattern is sent on a channel where the bot is.

Simple enough, let's get to the code:

session = Boty::Session.new
session.start do
  # ...

  hear("what's going on here?") do
    say "I don't know. But something is always wrong. :scream:"
  end
end

This configuration says that anytime a message containing the string "what's going on here?" is sent, the bot will respond. So a valid test could be send the following message in the #general channel:

valeriano 3:57 AM
OMG guys, what's going on here?

And the response will be, as expected:

jeeba BOT 3:57 AM
I don't know. But something is always wrong. :scream:

Wrapping up:

  • A listener is triggered for any message containing the pattern
  • A listener don't need a bot mention, so careful: the range of this config is wide

Before we start to study the "script" way of configuring command and listener binds, let's see how we can use ruby regexps to capture message parameters.

Regexes - a pattern that allow capture parameters within a message

What is a bot if it can't annoy people when we want it to? So let's teach our bot to send private messages to people in the Slack room.

Let's create an im command that will be capable of extract the person to whom we want to send a message, and the message that we want the bot send. Our bot will be capable of understand the following instruction:

valeriano 2:44 PM
@jeeba: im julian with omg! lol! bbq!

The command configuration can be like this:

command /im (\w+) with (.*)/ do |person, message|
  im message, to: person
end

The regex matched portions will be passed as parameter to the block given to command.

And this is it! :tada:

Commands, Listenners and aliases

Both commands and listeners can be aliased, which means that the same Action can be triggered by different regexes:

command /hi/i, /hello/i do
  say "Ohay #{user.name}! Hello there."
end
valeriano 12:04 PM
@jeeba: hi

jeeba BOT 12:04 PM
Ohay valeriano! Hello there.

valeriano 12:05 PM
@jeeba: hello

jeeba BOT 12:05 PM
Ohay valeriano! Hello there.

Adding custom scripts

Now you already know what are listeners and commands. It's time to know another way to create those "binders", it's what we call scripts.

Any ruby file available on scripts will be loaded when a Session starts. Your bot ships with an example script named script/ping.rb, with a very simple example of what you can do. But let's create a new script from scratch.

Create a new file script/flip.rb on your bot directory. Any method available in a bot will be available in this file, which means you can use command, hear, im, say, message, etc.

Let's code:

# script/flip.rb
command "flip" do
  say "(╯°□°)╯︵ ┻━┻"
end

And this is pretty much it. When your bot runs, it will be able to perform the command flip:

valeriano 3:36 PM
@jeeba: flip

jeeba BOT 3:36 PM
(╯°□°)╯︵ ┻━┻

So feel free to customize the bot file or create your own scripts. What please you more is what you should use.

DSL for bot scripting

In this section you will see all the methods available to configure your bot.

bot#say - Sending public messages on Slack channels

Use #say when the bot should send a message in the channel that triggered the configuration block:

command "flip" do
  say "(╯°□°)╯︵ ┻━┻"
end

If the bot wants to start a conversation, or even just say something in a channel without have received a message (via #hear or #command), just call #say inside a script.

say "ready to turn tables", channel: "#general"

command "flip" do
  say "(╯°□°)╯︵ ┻━┻"
end

If a channel isn't passed to #say it will defaults to "#general". You can use any Slack RTM postMessage parameter when calling #say.

bot#im

Bots can send private messages. Use the #im method:

command  "who am I?" do
  im "you are #{user.name}!"
end

In the previous example, the private message will be sent to the user that issued the command, this is a default. If you want the bot to send a message to a specific user, just indicate the user name via the to option:

command  "annoys julian" do
  im "Loren Ipsum, Julian boy!", to: "julian"
end

bot#message

The message that triggered a block is made available in the block's scope in the method #message:

command "what did you say?" do
  say "Well: #{message.text}"
end

Execute the previous command will be as follows:

valeriano 12:07 AM
@jeeba: what did you say?

jeeba BOT 12:07 AM
Well: @jeeba: what did you say?

Note that the #text returns the raw message that triggered the handler, including the bot mention itself.

bot#user

If the bot need to access the data about the user that send the message that triggered the handler, use the #user method.

Let me borrow an already used example:

command  "who am I?" do
  im "you are #{user.name}!"
end

bot#http

There is an utilitary method #http available on the handler block. This guy is useful to make, well... http requests. It supports all http verbs and return a Hash if the http response contains a "application/json" Content-Type header, or else the raw body.

Let's create a command that fetches a xkcd strip for us:

command /xkcd(\s\d+)?/ do |number|
  number = number ? number.strip : "1"
  xkcd = http.get "http://xkcd.com/#{number}/info.0.json"
  say "#{xkcd["alt"]}\n<#{xkcd["img"]}>"
end

To execute this command:

valeriano 12:40 AM
@jabberu: xkcd 2

The strip number 2 of xkcd will be brought to the room. If we don't pass a number parameter, the strip number 1 will be assumed as default.

If something goes wrong with the request, you can use the http#response method to access the inner response object. It'll be a faraday http response object.

bot#desc - Describing your bindings

A bot list all the commands and message handlers that it knows in the moment. If you want to give a nice description and/or a usage tip on command you can use the desc method.

Given that your bot has the following script:

desc "pug me", "Send some nice pug in the channel."
respond(/pug me/i) do
  # ...
end

The follow text will be part of the response for a @bot: knows command:

pug me: Send some nice pug in the channel.

You can use just the description if you want. In this case the regex itself will be used as the command name.

desc "Send some nice pug in the channel."
respond(/pug me/i) do
  # ...
end
valeriano 2:25PM
@bot: knows

bot 2:25PM
    knows: List all the commands known by this bot.
/pug me/i: Send some nice pug in the channel.

We strongly recommend that you describe all of your scripts. But if you don't, the bot will be capable of tell you what regexes are binded to it:

valeriano 2:47PM
@jabberu: knows

jabberu 2:47PM
                      knows: List all the commands known by this bot.
                  /pug me/i
/jabberu, are you there\?/i

Testing your own scripts

todo: document

For now, check the spec/script/ping_spec.rb and follow it's leads.

Running locally

After create the new bot application and give the bot api key, you can just enter the new dir and run

$ ./bot

Done! Your bot will be connected to your company Slack through the nickname that you provided in the integration step. To see if it's working properly, just go the the slack, in the general channel (or any other where the bot was invited) and type:

@jeeba: ping

It will respond to you: pong. IT'S ALIVE.

Deploy on heroku

But probably what you want is to have your bot running in a server, not in your machine, right?

Your bot is created with an example Procfile to make it easy to run it on Heroku (to give an example).

Create a new project on Heroku, following their instructions, until they ask you to do the "deploy" (the git push heroku master).

configure the api key

Now you need to add the configurations that are localy stored on .env.local. The two environment variables on that file are:

SLACK_COMPANY
SLACK_BOT_API_TOKEN

The Heroku command line tool offers a way to create environment vars in the server, check their documentation. Today, when as I'm writing the readme, you can use the following commands to set those two environment variables:

$ heroku config:set SLACK_COMPANY=your-company-name
$ heroku config:set SLACK_BOT_API_TOKEN=your-bot-integration-api-token

allow process to run

Heroku will not detect a web application in your bot, so you need to tell them to run your application as a, well... as a "normal" application. Go to your heroku dashboard (https://dashboard.heroku.com/apps), find the application that you have created to your bot. On the tab Resources, find a line with the information:

your-bot-name bundle exec ./bot

Turn on this resource. And done, your bot is up and running!

Using the logger

Boty ships with a builtin logger that can be used in your own scripts. It is made available via #logger. This will return a instance of a Ruby Logger.

Note that the default logger implementation will write to the STDOUT .Let's see a usage example:

session.start do
  command(/hello/i) do
    logger.debug "saying hello"
    say "hello there."
  end
end

By default the logger will write on the standard output, the previous command, when triggered, will produce the following log line:

D, [2015-12-11T00:13:28.712380 #40369] DEBUG -- : saying hello

Of course, if you need to write to a file, instead of the STDOUT, just change the adapter for your logger.

Log into files (instead of STDOUT)

Before start your bot session, configure the file log:

Boty::Logger.adapter = Logger.new("log/output.log")

And this is all you need! :tada:

Logger adapters

There is an adapter included on Boty that can be used by you and is also a good example of how to create a custom logger adapter for Boty.

Supose that you want to write to a file and still send the logs to the STDOUT. You can use the Multi adapter like this:

Boty::Logger.adapter = Boty::Logger::Multi.new([
  Logger.new(STDOUT),
  Logger.new("log/output.log")
])

And this is it. Now when you call logger.debug "some message" from your bot, this log line will be writen to the "log/output.log" file and also in the STDOUT.

You can write you own log adapter if you want. The Multi adapter implementation is fairly simple. Let's use it as an example of how to write your own adapter.

You can extend the ruby Logger class and worry yourself on write the #add override:

class Multi < ::Logger
  def initialize(adapters)
    @adapters = adapters
  end

  def add(*args, &block)
    @adapters.each do |adapter|
      adapter.add(*args, &block)
    end
  end
end

The add method is called internaly by Logger and has all the information that the auxiliary methods like #debug, #info etc received when called.

The Multi adapter implementation just delegate this call to the underlying adapters passed as parameters for the constructor.

For more information on the #add parameters, check the ruby doc.

Multi also allows you to change the level for all the underlying adapters at once, the #level= overriten implementation is like this:

def level=(level)
  @adapters.each do |adapter|
    adapter.level = level
  end
end

I18n

The descriptions for the commands and listeners that ship with Boty can be customized via the I18n gem mechanics. Boty ships with translations for :en and :pt-br. If you want to override those messages, you can take a look at the files on locale.

I'll recomend that you just copy and paste the file that you want to customize on the locale dir of your project, and them make the editions that you want.

Of course, you can add translations for any language that you want on your locale dir. And even add any translation that you want for your own command descriptions.

To instruct your bot about which language to use, set the Boty.locale configuration and you're done.

The bot executable created on your project generated by boty new have the following line before the session start:

Boty.locale = ARGV.pop || :en

This means that you can start your bot with a command line argument to tell which is the idiom that this session should use. This allows you to have the same bot running in different sessions with different idioms. The following usage is totally fine:

./bot pt-br

Development

After checking out the repo, run bin/setup to install dependencies. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

The local ./bin/bot

The project have a ./bin/bot executable that should be the product of compiling the template/project/bot.tt erb file. This is an easy way to just run the bot using the same logic that a project generated by bot new will use.

It's highly recomendable that you test your changes using this approach before submit a PR or generate a new release. =)

Compiling the ./bin/bot

There is a bot:compile rake task that compiles the template right into the bin direcotry and make it executable (with chmod +x).

Code guidelines

Just make sure to run bundle exec rubocop before submit any PR, good coding! :D

Contributing

Bug reports and pull requests are very welcome on GitHub at https://github.com/ricardovaleriano/boty. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The gem is available as open source under the terms of the MIT License.