A Perl script logs chat sessions

Minutes Taker

© Kheng Ho Toh, 123RF

© Kheng Ho Toh, 123RF

Author(s):

The IRC (Internet Relay Chat) protocol lets you program bots as automatic helpers. In this month's column, we let a bot log a session and store the results in a database.

At conferences, when supporting open source products, or whenever you need to coordinate a large number of project contributors, IRC is still the number one choice among instant messaging tools. None of its competitors – Yahoo, Microsoft, or the open Google Talk protocol – have been able to send the dinosaur among group communication tools into retirement.

The Bot::BasicBot module, which I introduced in a previous article [2] with a thermal sensor, handles communication between a Perl script and the IRC server so intelligently that programming the bot takes fewer than 10 lines of code. Bot::BasicBot::Pluggable takes this concept a stage further by adding plugin support to your bots so participants in a chat session can send messages to enable them. If a plugin is triggered, it performs the task assigned to it and sends the response back to the chat session.

Desperately Seeking …

The CPAN module has a handful of fully functional sample plugins that you can easily enable with load(). Listing 1 shows an implementation of a script that joins an IRC channel and enables two different plugins.

Listing 1

botstart

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Bot::BasicBot::Pluggable;
04
05 my $bot =
06   Bot::BasicBot::Pluggable
07   ->new(
08  channels =>
09    ["#perlsnapshot"],
10  server =>
11    "irc.freenode.net",
12  nick => "snapshot-bot",
13   );
14
15 # 'Seen' module: remembers
16 # when and where participants
17 # were last seen
18 $bot->load("Seen");
19
20 # DNS module: responds to
21 # "nslookup" messages by
22 # looking up IP addresses
23 $bot->load("DNS");
24
25 # Connect to IRC server
26 $bot->run();

The Seen plugin remembers the participants that log on and off and responds to the !seen username command with the time the participant was last online (Figure 1). This query is particularly useful if the user is no longer involved in the chat session. The second plugin enabled by botstart is DNS, which implements a simple interface to the Unix nslookup command and responds to !nslookup hostname commands with the IP address for the specified hostname as returned by the nslookup command-line utility (Figure 2).

The modularly extensible bot even accepts commands to load new plugins from the participants. If you configure the bot with $bot->load('Loader'), any participant can mess around with the bot's loading mechanism. When it receives a !load DNS message, the bot loads the DNS module; !unload DNS disables it again. Of course, this is risky, and although Bot::BasicBot::Pluggable tries to restrict users' options by providing an Auth module, this mechanism is easy to work around, as the documentation admits, and thus not recommended.

Can You Take a Message?

New plugins are easy to code. Bot authors can base new plugin classes on Bot::BasicBot::Pluggable. As you can see in Listing 2, you only need to overload a couple of methods to create a fully functional plugin. The init() method is called by the plugin framework once a script starts to bind the plugin. Because a pointer to the plugin object is passed into init(), it takes this opportunity to initialize its persistent data memory. For this purpose, it uses Cache::Historical from CPAN and stores a reference to it in the plugin's object data hash.

Listing 2

Log.pm

01 package Bot::BasicBot::Pluggable::Module::Log;
02 use warnings;
03 use strict;
04 use base qw(Bot::BasicBot::Pluggable             ::Module);
05 use Cache::Historical 0.03;
06 use Log::Log4perl qw(:easy);
07
08 our $SQLITE_FILE =
09   "irclog.dat";
10
11 #############################
12 sub init {
13 #############################
14  my ($self) = @_;
15
16  $self->{logbot_cache} =
17    Cache::Historical->new(
18     sqlite_file =>
19     $SQLITE_FILE);
20 }
21
22 #############################
23 sub help {
24 #############################
25  return
26    "Logs chats in SQLite";
27 }
28
29 #############################
30 sub told {
31 #############################
32  my ($self, $msg) = @_;
33
34  my $val =
35    "$msg->{who}: $msg->{body}";
36
37  my $key = $msg->{channel};
38  my $dt  =
39    DateTime->now(
40        time_zone => "local");
41
42  DEBUG "$dt $val";
43
44  $self->{logbot_cache}
45        ->set($dt, $key, $val);
46
47  return "";
48 }
49
50 1;

Later on, in the told() method, it picks up the reference again to store conversation snippets that it has captured. Cache::Historical was actually written to handle historical share prices, which it stores along with the date in an SQLite database, but the format is just as useful for chat messages, which are stored with a key (the name of the chat room – or "channel" in IRC lingo) and the current timestamp. The $SQLITE_FILE variable in line 8 of Listing 2 specifies the path to the SQLite file in the filesystem and might need to be converted to an absolute path or modified in some other way to match the local environment.

The help() method is mandatory and returns a short help text to tell the user what the plugin does.

Finally, the told() method in line 30 is called by the plugin framework whenever somebody says something in the chat room. Besides a pointer to the plugin object, told() is given a message hash with the sender ID and message content in the who and body keys. The module then calls the DateTime object's now() method to get the current time as a DateTime object – which is exactly the format in which Cache::Historical expects its timestamps. The time_zone parameter is set to "local" for the now() method to make sure that the timestamp is based in the current time zone. The told() method returns an empty string to the framework to indicate that it is not sending a message back to the chat room. The plugin is only interested in silently logging the conversation.

The script in Listing 3 starts a bot that loads the new log module and then joins a channel on the IRC server. Optionally, you can specify an array of channels that the bot joins at the same time. The load() method in line 23 will find the Log.pm module either in the current directory or in the framework's plugin directory, which is typically /usr/local/lib/perl5/site_perl/5.x/Bot/BasicBot/Pluggable. Log4perl sets the script's logging level to $ERROR in line 8; if you want the script to print the details of incoming messages and how it stored them for debugging purposes, you can change the level to $DEBUG.

Listing 3

logbot

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Bot::BasicBot::Pluggable;
04 use Log::Log4perl qw(:easy);
05
06 Log::Log4perl->easy_init(
07  {
08   level  => $ERROR,
09   layout => "%F{1}-%L %m%n"
10  }
11 );
12
13 my $bot =
14   Bot::BasicBot::Pluggable
15   ->new(
16  channels =>
17    ["#perlsnapshot"],
18  server =>
19    "irc.freenode.net",
20  nick => "snapshot-logger",
21   );
22
23 $bot->load("Log");
24
25 # Connect to IRC server
26 $bot->run();

If you want to see what the database content looks like on disk, you can peek behind the storage module's drapes: As you can see in Figure 4, it creates an SQLite database and a matching schema – without any intervention on the part of the bot programmer – and stores each incoming message in one line of a table. The entries are numbered sequentially with IDs and have a timestamp in column two, the chat room in column four, and the message itself in column five. The third column, upd_time, is not really interesting because Cache::Historical uses it for internal purposes. Because the module only provides one field in which to store the value, Log.pm simply bundles the sender and the message into a string, adding a colon and a blank to distinguish the two.

However, this format is not of much interest because Cache::Historical offers a values() method that returns all the messages stored under a specific key (the chat room) in reverse chronological order. Listing 4 shows the implementation, and Figure 5 shows the output from the script.

Listing 4

logdump

01 #!/usr/local/bin/perl -w
02 use strict;
03 use Cache::Historical 0.03;
04
05 our $SQLITE_FILE =
06   "irclog.dat";
07
08 my $cache =
09   Cache::Historical->new(
10  sqlite_file => $SQLITE_FILE,
11   );
12
13 for my $result (
14  $cache->values(
15   "#perlsnapshot")
16   )
17 {
18  my ($dt, $msg) = @$result;
19
20  print "$dt $msg\n";
21 }

Installation

The required modules, Bot::BasicBot:: Pluggable, Cache::Historical, and Log::Log4perl are available from CPAN and installed from the CPAN Shell, which automatically resolves dependencies. In the logbot script, you need to modify the name of the chat room you want to monitor and the IRC server you will use before you launch the bot. The Cache::Historical module automatically sets up the SQLite database and the required schema. Other than modifying the $SQLITE_FILE variable to match the path to the database file, no preparation is required. Now you can eavesdrop on conferences and know what people are talking about behind your back, even while you are presenting! The script doesn't even need to run on the laptop you are using for your talk (which could have WiFi disabled): It can be launched somewhere on the Internet and its results checked at a later time.

Infos

  1. Listings for this article: ftp://www.linux-magazin.de/pub/listings/magazin/2009/11/Perl
  2. "How Cool is Perl?" by Michael Schilli, Linux Magazine, April 2006, http://www.linuxpromagazine.com/Issues/2006/65/HOW-COOL-IS-PERL

The Author

Michael Schilli works as a software engineer with Yahoo! in Sunnyvale, California. He is the author of Goto Perl 5 (German) and Perl Power (English), both published by Addison-Wesley, and he can be contacted at mailto:mschilli@perlmeister.com. Michael's homepage is at http://perlmeister.com.

Read full article as PDF:

Related content

  • Perl: amtrack

    If you are a bargain hunter, you might enjoy this Perl script that monitors price developments at Amazon and alerts you if Amazon suddenly drops the prices on the products you have been watching.

  • Perl: Portfolio Watch

    We'll show you a Perl script that helps you draw area graphs to keep track of your portfolio's performance.

  • Perl: Personal Finances

    A helpful Perl script gives you an immediate overview of your financial status, adding the balances of multiple accounts and share depots. It even allows users to add their own plugins.

  • Perl: Link Spam

    Spammers don’t just send email. They exploit discussion forums and blogs, posting pseudo-messages full of links to dupe search engines. A Perl script cleans up the mess.

  • Perl: A Gaim Plugin

    The Gaim project offers an instant messenger client that speaks a large number of protocols. We’ll show you how to extend Gaim with Perl plugins.

comments powered by Disqus

Direct Download

Read full article as PDF:

News