Building an IRC Bot with Cinch

Bot Tech

By

Chat rooms aren’t just for people. We’ll show you how to access an IRC channel using an automated bot.

IRC, the Internet Relay Chat has existed for 20 years, and it is still a popular communication channel in open source projects and businesses. For almost as long, automated bots have listened on IRC channels and responded to user commands. The article shows how IRC bots can make themselves useful by helping you manage tickets and prepare documentation.

Spoiled for Choice

To develop an IRC bot today, you no longer need to learn Tcl to activate the bot ancestor Eggdrop. Frameworks for bots are available in virtually any language (Table 1). Although I focus here on Cinch, a framework written in Ruby under the MIT license (Figure 1), you can easily adapt the code examples presented in this article to other languages.

Table 1: A Selection of IRC Bots

Name of Bot

Programming Language

License

Website

Autumn

Ruby

Freeware

https://github.com/RISCfuture/autumn

Willie

Python

EFL

http://willie.dftba.net

PHP IRC Bot

PHP

CC-BY-3.0

http://wildphp.com

Jsircbot

JavaScript

GPLv2

http://code.google.com/p/jsircbot/

Java IRC Bot

Java

GPLv2

http://sourceforge.net/projects/jircb/

Figure 1: The simplified structure of the Cinch Ruby bot.

Cinch comes with an object-oriented API and a modular plugin system. Thanks to many unrelated plugins, a single bot can perform all kinds of tasks. To an IRC server, Cinch – as is typical of IRC bots – appears as a normal client. Therefore, it does not matter which system the bot runs on, whether on the same system as the IRC server or remotely on a developer machine. Nor does it matter which server software you use, as long as the server is a compliant implementation of IRC.

Bot on Board

Assuming you have a Ruby installation in place, you can install Cinch as follows:

gem install cinch

A simple bot named hellobot.rb (Figure 2) that responds to salutations in the form of !hello is presented in Listing 1 (code is available online).

Figure 2: A perpetual responder in its simplest form.

Listing 1: hellobot.rb

# -*- coding: utf-8 -*-
require "cinch"
  
class Greeter
    include Cinch::Plugin
  
    match /hello$/, method: :greet
    def greet(m)
    m.reply "Hi there"
    end
end
  
bot = Cinch::Bot.new do
    configure do |c|
        c.nick = "<OurBot>"
        c.server = "<IRC_server_address>"
        c.channels = ["<#a_channel>", "<#another_channel>"]
        c.plugins.plugins = [Greeter]
    end
end
  
bot.start

Typing ruby hellobot.rb brings the bot to life; it then connects with the IRC server <IRC_server_address> and joins the channels <#a_channel> and <#another_channel>. If an IRC user types !hello in one of these channels, the bot responds with a rather indifferent Hi there.

The sample code consists of two parts: The main part is the Greeter class (lines 4-11). Each class represents a single plugin that responds to one or more commands from the user; a command must begin with an exclamation mark. To match the commands, bot developers rely on regular expressions. For example, /(\d+)/ is a single argument comprising one or multiple numbers.

The second part (lines 13-20) takes care of configuring the bot. The program receives a name and an address at which it can log in. Line 18 also explains what plugins the bot should use. The configuration of the file does not change in the remainder of the article, so I can now move on to the actual plugin classes. Developers can easily extend the array of plugins; for more information on the API, check out the documentation.

GitHub Connection

Developers spend a huge amount of time processing tickets; little wonder their conversations often revolve around this topic. This article will show you how to build a bot that will control the GitHub ticket system. A plugin will support opening, closing, and finding tickets. At the same time, I want the bot to respond to references of the type repository/gh-ticket_number in messages by displaying the title status of the ticket in the channel.

GitHub’s API is based on HTTP and JSON and allows both read and write access to large parts of GitHub – including tickets. You can experiment with access using curl at the command line; Listing 2, for example, requests ticket number 13069 from the Rails project.

Listing 2: Ticket Request with curl

$ curl https://api.github.com/repos/rails/rails/issues/13069
{
    "title": "Requires JSON gem version 1.7.7 or above as it contains an important security fix.",
    "user": {
        "login": "chancancode",
        <[...]>
    },
    "labels": [],
    "state": "closed",
    "created_at": "2013-11-27T06:20:30Z",
    "updated_at": "2013-11-27T10:07:54Z",
    "closed_at": "2013-11-27T10:07:54Z",
    "body": "See [here](https://github.com/flori/json/blob/master/CHANGES).",
    [...]
}

This (abridged) example shows how GitHub structures the information the bot requires to display tickets. The developer can access both public information (such as tickets from open source projects) as well as private tickets. In the second case, however, you do need to authenticate. The API allows authentication either via O-Auth or classic HTTP, which is usually sufficient for an IRC bot. For in-house installations of GitHub, access to the API looks like this: http://<IP_address>/api/v3/.

Tell Your API

At its core, the plugin communicates with the GitHub API. It retrieves the data via HTTP and translates JSON into Ruby structures. Since the standard libraries of Ruby provide everything you need, the implementation is easy.

Lines 7-10 in Listing 3 handle the configuration of the plugin. BaseURL is the base address of the GitHub API and requires some adjustment for in-house installations. This entry is followed by the Username and Password for the bot on GitHub. The account must be authorized to access tickets in repositories, but it should not have more rights than necessary.

Listing 3: issues.rb

require "json"
require "net/http"
  
class GithubIssues
    include Cinch::Plugin
  
    BaseURL      = "api.github.com"
    Organization = "<Organization>"
    Username     = "<username>"
    Password     = "<password>"
  
    private
    def request(uri, method, data = nil)
        uri = URI("https://#{BaseURL}#{uri}")
        Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
            req = method.new(uri.request_uri)
            req.basic_auth(Username, Password)
            req.body = data
            resp = http.request(req)
            return JSON.parse(resp.body)
        end
    end
end

To simplify the interaction with the bot, the assumption is that all the repositories at GitHub belong to a single Organization. The request() method in line 13 is used by all of the plugin’s functions to make requests to the API. request() issues an HTTP request and converts the response, which is formatted in JSON, to a structure that Ruby understands. The method argument is used to specify the HTTP command. GitHub uses, for example, GET to retrieve data and PATCH to edit data.

The plugin’s functions are restricted to calling this method along with the right arguments. You then extend the GithubIssues class with the methods from Listings 4 through 6 in the file issues.rb. The first two functions, which open and close tickets, essentially consist of the call to the correct request() method.

Listing 4: Supplement 1 for issues.rb

# !gh issue open <Repository> <Ticket_number>
match(/gh issue open ([^ ]+) (\d+)$/, method: :open_issue)
def open_issue(m, repo, id)
    uri = "/repos/#{Organization}/#{repo}/issues/#{id}"
    request(uri, Net::HTTP::Patch, '{"state": "open"}')
    m.reply "Opened issue %s/gh-%d" % [repo, id]
end
  
# !gh issue close <Repository> <Ticket_number>
match(/gh issue close ([^ ]+) (\d+)$/, method: :close_issue)
def close_issue(m, repo, id)
    uri = "/repos/#{Organization}/#{repo}/issues/#{id}"
    request(uri, Net::HTTP::Patch, '{"state": "closed"}')
    m.reply "Closed issue %s/gh-%d" % [repo, id]
end

Listing 5: Supplement 2 for issues.rb

# !gh issue search <Repository> <search_string>
match(/gh issue search ([^ ]+) (.+)/, method: :search_issue)
def search_issue(m, repo, query)
    uri = "/search/issues?q=%s+repo:%s+user:%s" %
        [URI.escape(query), repo, Organization]
  
    res = request(uri, Net::HTTP::Get)
    n = 3
    total = res["total_count"]
    if n > total
        n = total
    end
  
    m.reply "Showing %d of %d tickets for '%s'" % [n, total, query]
    res["items"][0...n].each_with_index do |issue, i|
        # [123] Ticket title <https://github.com/...> (open)
        m.reply "[%d] %s <%s> (%s)" %
            [issue["number"], issue["title"], issue["html_url"], issue["state"]]
    end
end

To search for tickets (Figure 3), the developer needs to do some more work. Although the actual search consists of one simple call to the request() method, the code needs to present the search results appropriately. The program should not show all the results – there could be up to 100 – and it should show only the relevant information for each ticket, such as the ticket number, the title, the link to the ticket, and the status (open or closed).

Figure 3: The bot scanning the GitHub ticket system for specific tickets.

The final feature (Listing 6) finds all references of the type Repository/gh-Ticket_number in chat messages and outputs the title and status of the referenced tickets. Ruby’s scan() method finds all references to a message so that a single message can also cover several tickets (Figure 4). The special use_prefix: false option ensures that the bot does not look for an exclamation mark at the start of the command.

Listing 6: Supplement 3 for issues.rb

# <Repository>/gh-<Ticket-number>
match(/[^ ]+\/gh-\d+/, method: :display_issue, use_prefix: false)
def display_issue(m)
    m.message.scan(/([^ ]+)\/gh-(\d+)/) do |repo, id|
        uri = "/repos/#{Organization}/#{repo}/issues/#{id}"
        issue = request(uri, Net::HTTP::Get)
        m.reply "[%s/gh-%d] %s (%s)" % [repo, id, issue["title"], issue["state"]]
    end
end
Figure 4: When you enter a ticket number, the bot returns the matching titles.

Caveats

The plugin developed here makes it much easier for developers and admins to work through IRC chatrooms by performing targeted searches for information about certain tickets. However, many improvements are possible. For example, the plugin assumes that the repositories and tickets exist and that GitHub’s API never has any problems. In short, the plugin does not provide error handling. Additionally, the number of requests to GitHub’s API is limited to 5,000 per hour. Very active projects and large companies would have to cache requests or otherwise comply with the limits.

What the Bot Knows

Of course, logs of the conversations on IRC can be referenced in case of questions. It is also possible to collect FAQ knowledge on IRC and retrieve this information as needed. Bots can manage the FAQ. Inspired by Infobot the electronic aids connect snippets of text – such as links or references – with retrievable keywords. If the developer saves these questions and answers in a compact SQLite database, other tools can access the data.

The implementation of the knowledgebase function is similar to that of the GitHub link. The task starts with establishing a database connection, and the actual functions follow. Install the database with:

gem install sqlite3

Listing 7 opens a connection to the SQLite database, which is located in the home directory of the user. The bot developer’s plugin creates a table with three columns for the ID, the keyword, and the associated information, assuming this table does not yet exist. The plugin is then used without any manual intervention.

Listing 7: infobot.rb

require "sqlite3"
  
class Infobot
    include Cinch::Plugin
  
    DB = ENV["HOME"] + "/infobot.db"
  
    def initialize(*args)
        @db = SQLite3::Database.new(DB)
        @db.execute(
            "CREATE TABLE IF NOT EXISTS infobot(
            id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
            term TEXT NOT NULL,
            value TEXT NOT NULL
        )")
        super
    end
end

Listing 8 implements two methods for creating and retrieving information. The first method, remember(), is always used when somebody uses !rem keyword = information to feed new snippets of information to the bot – or to overwrite existing information and thus correct an error.

Listing 8: Supplement for infobot.rb

# !rem keyword = information
match(/rem ([^ ]+) = (.+)$/, method: :remember)
def remember(m, term, val)
    @db.execute("INSERT OR REPLACE INTO infobot (term, value) VALUES (?, ?)", term, val)
    m.reply("Okay, #{term} now means #{val}")
rescue => e
    exception(e)
    m.reply("Error remembering '#{term}'")
end
  
# !<keyword>
match(/([^ ]+)$/, method: :lookup)
def lookup(m, term)
    res = @db.get_first_value("SELECT value FROM infobot WHERE term = ?", term)
    if res
        m.reply(res)
    end
end

The second method, lookup(), activates the bot when it sees a message of the !keyword type. If lookup() recognizes the keyword, it outputs its meaning; otherwise it remains silent. A typical interaction with the bot will look something like Listing 9, which demonstrates how IRC users store and retrieve information. Because the data also resides in a schematically simple SQLite database, access to knowledge is not limited to the bot. A console client could list the keywords, and data could be imported from existing sources, such as wikis. However, whole paragraphs tend to get in the way on IRC. Infobots are intended to output short sentences but not novels.

Listing 9: Knowledgebase on IRC

<Max> !rem deploy = Pay attention to X and Y when deploying
<Susanne> !rem github_api = http://developer.github.com/v3/
[a few days later]
<Max> Hmm, where was the GitHub API documentation ...
<Max> !github_api
<Bot> http://developer.github.com/v3/

Conclusions

Just 150 lines of code are enough to turn a chatroom into a helpful tool for development and documentation, and you will find many other ways to facilitate work using IRC bots. Whether tracking connectivity, managing memos, or checking in for home office workers, IRC bots can save you time and valuable attention. Moreover, because libraries exist for a variety of popular languages, developers do not have to learn new skills to program a bot.

Author

Dominik Honnef has been active on IRC for 10 years. Three years ago, he developed Cinch and has since helped developers design their own plugins.

Related content

  • Building an IRC Bot

    Chat rooms aren't just for people. We'll show you how to access an IRC channel using an automated bot.

  • Trouble Ticket Software

    If your help line serves outside users, keeping track of support requests can mean the difference between a repeat customer and a lost customer. If the line serves inside employees, an efficient response means better productivity. Fortunately,several Linux-based applications offer help for your help desk or hotline.

comments powered by Disqus
Subscribe to our Linux Newsletters
Find Linux and Open Source Jobs
Subscribe to our ADMIN Newsletters

Support Our Work

Linux Magazine content is made possible with support from readers like you. Please consider contributing when you’ve found an article to be beneficial.

Learn More

News