Version Tests Code Climate Donorbox

Rodbot

Rodbot

Minimalistic yet polyglot framework to build chat bots on top of a Roda backend for chatops and fun.


⚠️ RODBOT IS UNDER CONSTRUCTION AND NOT FIT FOR ANY USE YET.
🚧 Active development is underway, the first release should be ready soonish.

Table of Contents

Install
Anatomy
   App Service
   Relay Services
   Schedule Service
CLI
Routes and Commands
Database
Plugins
Environment Variables
Development

Install

Security

This gem is cryptographically signed in order to assure it hasn't been tampered with. Unless already done, please add the author's public key as a trusted certificate now:

gem cert --add <(curl -Ls https://raw.github.com/svoop/rodbot/main/certs/svoop.pem)

Generate New Bot

Similar to other frameworks, generate the files for your new bot as follows:

gem install rodbot --trust-policy MediumSecurity
rodbot new my_bot
cd my_bot
bundle install
bundle exec rodbot --help

Anatomy

The bot consists of three kinds of services interacting with one another:

RODBOT                                                            EXTERNAL
╭╴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╶╮
╷ ╭──────────────────╮  <─────>  ╭──────────────────╮       ╷
╷ │ APP              │  <───╮    │ RELAY - Matrix   ├╮  <──────>  [1] Matrix
╷ ╰──────────────────╯  <─╮ │    ╰┬─────────────────╯├╮  <──┼──>  [1] simulator
╷                         │ │     ╰┬─────────────────╯│   <────>  [1] ...
╷                         │ │      ╰──────────────────╯     ╵
╷                         │ │                               ╵
╷                         │ │     ╭──────────────────╮      ╵
╷                         │ ╰──>  │ SCHEDULE         │  <───┼───  [2] clock
╷                         │       ╰──────────────────╯      ╷
╷                         │                                 ╷
╷                         ╰─────────────────────────────────────  [3] webhook caller
╰╴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ╶╯

App Service

The app service is a Roda app where the real action happens. It acts on and responds to HTTP requests from:

  • commands forwarded by relay services
  • timed events triggered by the schedule service
  • third party webhook calls e.g. from GitLab, GitHub etc

Commands

All top level GET requests such as GET /foobar are commands and therefore are accessible by relays, for instance using !foobar on Matrix.

Responses have to be either of the following content types:

  • text/plain; charset=utf-8
  • text/markdown; charset=utf-8

Please note that the Markdown might get stripped on communication networks which feature only limited or no support for Markdown.

The response may contain special tags which have to be replace appropriately by the corresponding relay service:

Tag Replaced with
[[SENDER]] Mention the sender of the command.

Other Routes

All higher level requests such as GET /foo/bar are not accessible by relays. Use them to implement other aspects of your bot such as webhooks or schedule tasks.

Relay Services

The relay service act as glue between the app service and external communication networks such as Matrix.

Each relay service does three things:

  • Proactive: It creates and listens to a local TCP socket. Plain text or Markdown sent to this socket is forwarded as a message to the corresponding communication network. This text may have multiple lines, use the EOT character (\x04 alias Ctrl-D) to mark the end.
  • Reactive: It reads messages, detects commands usually beginning with a !, forwards them to the app service and writes the HTTP response back as a message to the communication network.
  • Test: It detects the !ping command and replies with "pong" without hitting the app service.

You can simulate such a communication network locally:

rodbot simulator

Enter the command !pay EUR 123 and you see the request GET /pay?argument=EUR+123 hitting the app service.

Schedule Service

The schedule service is a Clockwork process which triggers HTTP requests to the app service based on timed events.

CLI

The rodbot CLI is the main tool to manage your bot. For a full list of functions:

rodbot --help

Starting and Stopping Services

While working on the app service, you certainly want to try routes:

rodbot start app

This starts the server in the current terminal. You can set breakpoints with binding.irb, however, if you prefer a real debugger:

rodbot start app --debugger

This requires the debug gem and adds the ability to set breakpoints with debugger.

You can also start single services in the background:

rodbot start app --daemonize

However, it's not particularly useful unless you start all services at once. In fact, it's even mandatory in this case, so you don't have to mentioe --daemonize explicitly:

rodbot start

Finally, to start all running Rodbot services:

rodbot stop

Deployment

There are many ways to deploy Rodbot on different hosting services. For the most common scenarios, you can generate the deployment configuration:

rodbot deploy docker

In case you prefer to split each service into its own container:

rodbot deploy docker --split

Routes and Commands

Adding new tricks to your bot boils down to adding routes to the app service which is powered by Roda, a simple yet very powerful framework for web applications: Easy to learn (like Sinatra) but really fast and efficient. Take a minute and get familiar with the basics of Roda.

Rodbot relies on MultiRun to spread routes over more than one routing file. This is necessary for Rodbot plugins but is entirely optional for your own routes.

⚠️ At this point, keep in mind that any routes at the root level like /pay or /calculate can be accessed via chat commands such as !pay and !calculate. Routes which are nested further down, say, /myapi/users are off limits and should be used to trigger schedule events and such. Make sure you don't accidentally add routes to the root level you don't want people to access via chat commands, not even by accident.

To add a simple "Hello, World!" command, all you have to do is add a route /hello. A good place to do so is app/routes/hello.rb:

module Routes
  class Hello < App

    route do |r|

      # GET /hello
      r.root do
        response['Content-Type'] = 'text/plain; charset=utf-8'
        'Hello, World!'
      end

    end

  end
end

To try, start the app service with rodbot start app and fire up the simulator with rodbot simulator:

rodbot> !hello
Hello, World!

Try to keep these route files thin and extract the heavy lifting into service classes. Put those into the lib directory where they will be autoloaded by Zeitwerk.

Database

Your bot might be happy dealing with every command as an isolated event. However, some implementations require data to be persisted between requests. A good example is the OTP plugin which needs a database to assure each one-time password is accepted once only.

Rodbot implements a very simple key/value database. Currently, the following backends are supported:

  • Redis
  • Hash (not thread-safe, don't use it in production)

To enable a database, uncomment the Redis gem in gems.rb, then add the following configuration to config/rodbot.rb:

db 'redis://localhost:6379/10'

With this in place, you can access the database with Rodbot.db:

Rodbot.db.flush                                        # => Rodbot::Db

Rodbot.db.set('foo') { 'bar' }                         # => 'bar'
Rodbot.db.get('foo')                                   # => 'bar'
Rodbot.db.scan('*')                                    # => ['foo']
Rodbot.db.delete('foo')                                # => 'bar'
Rodbot.db.get('foo')                                   # => nil

Rodbot.db.set('lifetime', expires_in: 1) { 'short' }   # => 'short'
Rodbot.db.get('lifetime')                              # => 'short'
sleep 1
Rodbot.db.get('lifetime')                              # => nil

For a few more tricks, see the Rodbot::Db docs.

Plugins

Rodbot aims to keep its core small and add features via plugins, either built-in or provided by gems.

Built-In Plugins

Name Description
:matrix relay with the Matrix communication network
:say write proactive messages to communication networks
:otp guard commands with one-time passwords
:gitlab_webhook event announcements from GitLab
:github_webhook event announcements from GitHub
:hal feel like Dave (demo)
:word_of_the_day word of the day announcements (demo)

How Plugins Work

Given the following config/rodbot.rb:

plugin :my_plugin do
  color 'red'
end

Plugins provide one or more extensions each of which extends one of the services. In order only to spin things up when needed, the plugin may contain the following files:

  • rodbot/plugins/my_plugin/app.rb – add routes and/or extend Roda
  • rodbot/plugins/my_plugin/relay.rb – add a relay
  • rodbot/plugins/my_plugin/schedule.rb – add schedules to Clockwork

Whenever a service boots, the corresponding file is required.

In order to keep these plugin files slim, you should extract functionality into service classes. Just put them into rodbot/plugins/my_plugin/lib/ and use require_relative where you need them.

Create Plugins

You can create plugins in any of the following places:

  • inside your Rodbot instance:
    /lib/rodbot/plugins/my_plugin
  • in a vendored gem "rodbot-my_plugin":
    /lib/rodbot/vendor/gems/rodbot-my_plugin/lib/rodbot/my_plugin
  • in a published gem "rodbot-my_plugin":
    /lib/rodbot/plugins/my_plugin

Please adhere to common naming conventions and use the dashed prefix rodbot- (and Module Rodbot), however, underscores in case the remaining gem name consists of several words.

App Extension

An app extension rodbot/plugins/my_plugin/app.rb looks something like this:

module Rodbot
  class Plugins
    module MyPlugin
      module App

        module Routes < Roda
          route do |r|
            # GET /my_plugin
            r.root do
              # called by command !my_plugin
            end

            # GET /my_plugin/whatever
            r.get('whatever') do
              # not reachable by any command
            end
          end
        end

        module ResponseMethods
          # (...)
        end

      end
    end
  end
end

The Routes module contains all the routes you would like to inject. The above corresponds to GET /my_plugin/hello.

The App module can be used to extend all aspects of Roda.

For an example, take a look at the :hal plugin.

Relay Extension

A relay extension rodbot/plugins/my_plugin/relay.rb looks something like this:

module Rodbot
  class Plugins
    module MyPlugin
      class Relay < Rodbot::Relay

        def loops
          SomeAwesomeCommunicationNetwork.connect
          [method(:read_loop), method(:write_loop)]
        end

        private

        def read_loop
          loop do
            # Listen in on the communication network
          end
        end

        def write_loop
          loop do
            # Post something to the communication network
          end
        end

      end
    end
  end
end

The loops method must returns an array of callables (e.g. a Proc or Method) which will be called when this relay service is started. The loops must trap the INT signal.

Proactive messsages require other parts of Rodbot to forward a message directly. To do so, the relay has to implement a TCP socket. This socket must bind to the IP and port you get from the bind method which returns an array like ["localhost", 16881].

For an example, take a look at the :matrix plugin.

Schedule Extension

A schedule extension rodbot/plugins/my_plugin/schedule.rb looks something like this:

module Rodbot
  class Plugins
    module MyPlugin
      module Schedule

        # (...)

      end
    end
  end
end

Please note: Schedules should just call app service routes and let the app do the heavy lifting.

TODO: needs further description and examples

For an example, take a look at the :word_of_the_day plugin.

Environment Variables

Variable Description
RODBOT_ENV Environment (default: development)
RODBOT_CREDENTIALS_DIR Override the directory containing encrypted credentials files
RODBOT_SPLIT Split deploy into individual services when set to "true" (default: false)

Development

To install the development dependencies and then run the test suite:

bundle install
bundle exec rake    # run tests once
bundle exec guard   # run tests whenever files are modified

Some tests require Redis and will be skipped by default. You can enable them by setting the following environment variable along the lines of:

export RODBOT_SPEC_REDIS_URL=redis://localhost:6379/10

You're welcome to join the discussion forum to ask questions or drop feature ideas, submit issues you may encounter or contribute code by forking this project and submitting pull requests.