Building an IRC Bot with Cinch
Bot Tech
ByChat 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 |
|
Willie |
Python |
EFL |
|
PHP IRC Bot |
PHP |
CC-BY-3.0 |
|
Jsircbot |
JavaScript |
GPLv2 |
|
Java IRC Bot |
Java |
GPLv2 |
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).
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).
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
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.
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.
News
-
Gnome Fans Everywhere Rejoice for the Latest Release
Gnome 47.2 is now available for general use but don't expect much in the way of newness, as this is all about improvements and bug fixes.
-
Latest Cinnamon Desktop Releases with a Bold New Look
Just in time for the holidays, the developer of the Cinnamon desktop has shipped a new release to help spice up your eggnog with new features and a new look.
-
Armbian 24.11 Released with Expanded Hardware Support
If you've been waiting for Armbian to support OrangePi 5 Max and Radxa ROCK 5B+, the wait is over.
-
SUSE Renames Several Products for Better Name Recognition
SUSE has been a very powerful player in the European market, but it knows it must branch out to gain serious traction. Will a name change do the trick?
-
ESET Discovers New Linux Malware
WolfsBane is an all-in-one malware that has hit the Linux operating system and includes a dropper, a launcher, and a backdoor.
-
New Linux Kernel Patch Allows Forcing a CPU Mitigation
Even when CPU mitigations can consume precious CPU cycles, it might not be a bad idea to allow users to enable them, even if your machine isn't vulnerable.
-
Red Hat Enterprise Linux 9.5 Released
Notify your friends, loved ones, and colleagues that the latest version of RHEL is available with plenty of enhancements.
-
Linux Sees Massive Performance Increase from a Single Line of Code
With one line of code, Intel was able to increase the performance of the Linux kernel by 4,000 percent.
-
Fedora KDE Approved as an Official Spin
If you prefer the Plasma desktop environment and the Fedora distribution, you're in luck because there's now an official spin that is listed on the same level as the Fedora Workstation edition.
-
New Steam Client Ups the Ante for Linux
The latest release from Steam has some pretty cool tricks up its sleeve.