Previous Section  < Day Day Up >  Next Section

Hack 15 Find Users in Channels

figs/expert.gif figs/hack15.gif

Finding someone on IRC is not always that easy, particularly if you don't know his exact nickname. Write some scripts and discover who's in a channel.

Sooner or later, you will probably want to write an IRC gadget that finds specific people in a channel (and possibly sends them a message or does something else to them). In a usual scenario, you can get the list of people dwelling in the channel quite easily, with varying efficiency depending on what approach you take.

Let's take a look at the problem from several different perspectives and see how to solve it in various programming environments.

3.5.1 Nick Seeking

You could have different criteria for your search. Perhaps you just want to examine whether a user with a given nick is around on the channel. For example, you could be writing a simple !seen robot, which records the time of the last visit of a given person (usually identified by her nick), and you want to check whether the queried nick is actually present on the channel.

If you are interested only in the nick, you could use the NAMES #channel command. This is the command that is automatically executed whenever you join a channel. The command returns a few lines of 353 numeric, ending with the 366 numeric [Hack #78] . One sample line could look like this:

:x.fn.net 353 nickseek @ #irchacks :DeadEd MD87 Monty dg @Jibbler +elvum +pasky

As you can see, the command reveals not only the nicknames of all users in that channel, but also their status. Channel operators will have nicknames that start with @ , half-opped users will start with %, and voiced users will start with +. However, there's a danger here: if the user is both opped and voiced, only the op status will be shown in the NAMES list. When the user loses his op status, he will still be voiced, but you will not know about it. Even some popular IRC clients suffer from this problem, unfortunately.

3.5.2 Advanced Search

If you want anything more than just a nickname, NAMES will not be very helpful. You could get more information by sending a WHOIS for each individual nickname, but that would be tedious, especially on larger channels. You will need to use something more elaborate, and the WHO #channel command is a perfect fit. It returns each user on a separate line (as a set of 352 numerics and terminated by a 315 numeric) with a rich set of additional information, for example:

:x.fn.net 352 nickseek #irchacks ~pasky pasky.or.cz irc.fn.net pasky H+ :0 IRC Name

The first bit of useful information you get to see is the host mask. This shows where the user is coming from—~pasky@pasky.or.cz in this case. It is followed by the name of the server that the user is connected to. Some IRC networks (freenode for example) have hidden internal topology to prevent targeted DDoS attacks—in that case, this item will always be irc.freenode.net and is mostly meaningless.

Next is the user's nickname (pasky) and a flag indicating whether the user is Here or Gone. A user can change this flag by using the AWAY command, which is typically invoked by typing /away reason in an IRC client. The user status (op, half-op, voice) is also appended to this flag, if appropriate. A * will be appended to the flag for IRC operators. After the colon is a number that represents the distance between your server and the other user's server (see the earlier remark about hidden network topology). Everything after that represents the user's IRC name.

3.5.3 A Strategy for Finding Users

You already know how to extract the necessary information from an IRC channel, so now the question is when to extract it. This largely depends on the purpose of your project, but there are two feasible approaches. One is to just execute the WHO command every time you need to check the channel list. This is a very simple approach that certainly works; however, it is terribly inefficient and becomes a bottleneck when you need to check the list very often.

The alternative approach is to capture the WHO output once and then watch the JOIN , PART, KICK, QUIT, and MODE commands, updating the in-memory list on your own. This list tracking is more complicated, but if you need to get the list more frequently, it is the only sensible way to do it. Of course, you won't be able to monitor the status of the AWAY flag using this approach, but that is not too much of a loss.

3.5.4 The Code

The first piece of code for this hack is based on the ultimate shell IRC client [Hack #41] . You can implement a user lookup in this script. You will need to add the lookup function to the script:

function lookup ( ) {

    chan=$1;

    host=$2;

    echo "WHO $chan"

    while read input; do

        input=`echo "$input" | tr -d '\r\n'`



        # WHO item

        num=`echo "$input" | cut -d " " -f 2`

        if [ "$num" -eq "352" ]; then

            thishost=`echo "$input" | cut -d " " -f 6`

            if [ "$host" = "$thishost" ]; then

                return 0;

            fi

        fi



        # Stop WHO

        if [ "$num" -eq "315" ]; then

            break;

        fi

    done

    return 1;

}

This function checks whether there is anyone on a channel from a given host. This way, it should be trivial to alter the code to match for different criteria. Note that, ideally, the numeric checking should be part of the main input loop instead of having another one in the function, since the server could send us anything between the WHO request and the delivery of the first 352 numeric.

3.5.4.1 Finding users with Net::IRC

Shell scripts aren't everyone's favorite cup of tea, so now you can implement the same function for Net::IRC [Hack #33], by adding a few extra features along the way. Instead of looking for something specific in the WHO output, you can just insert the entire output into a hash:

# Indexed by nick, contains list of people.

use vars qw (%userlist);



# Working copy, it is copied to %userlist when complete.

my %who;



sub on_whoreply {

  my ($self, $event) = @_;



  # Split the WHO reply message into its separate arguments.

  my ($me, $chan,  $ident, $host, $server, $nick, $flags, $data)

      = $event->args ( );

  my (@z) = $event->args ( );



  # Process the flags.

  my ($gone, $serverop, $op, $halfop, $voice) = (0, 0, 0, 0, 0);

  foreach my $flag (split (//, $flags)) {

    if ($flag eq 'G') { $gone = 1; next; }

    if ($flag eq '*') { $serverop = 1; next; }

    if ($flag eq '@') { $op = 1; next; }

    if ($flag eq '%') { $halfop = 1; next; }

    if ($flag eq '+') { $voice = 1; next; }

  }



  # Process the ircname and hopcount.

  my ($hops, $realname) = split (/ /, $data, 2);



  # Insert the newly extracted record to a working user list.

  $who{$nick} = {

    host => $ident . '@' . $host, server => $server,

    gone => $gone, serverop => $serverop,

    op => $op, halfop => $halfop, voice => $voice,

    hops => $hops, realname => $realname

  };

}



$conn->add_handler ('whoreply', \&on_whoreply);



sub on_endofwho {

  my ($self, $event) = @_;



  # The working user list (%who) is ready, so switch over the main one.

  %userlist = %who;

}



$conn->add_handler ('endofwho', \&on_endofwho);



# This triggers the update.

sub update_userlist {

  my ($conn, $channel) = @_;



  # Clean up the working user list.

  %who = ( );

  $conn->who($channel);

}

When you want to fill the %userlist hash, you simply have to call update_userlist($conn, '#channel'). It may take a few seconds to actually get the results you're after, so you can put a hook to on_endofwho() in order to get notified once the operation is complete. The obvious issue with the preceding code is that it does not work if you want to work on more than one channel at once. Fixing this is left as an exercise for the reader, but it should be trivial—just extend %userlist and %who to be indexed by a channel name first.

3.5.4.2 irssi

If you are making scripts for the irssi IRC client [Hack #24], you will surely be delighted to read that the client keeps track of the user list for you. First, you need to get the channel object—irssi passes it to your event hook, or you can get it by doing this:

$server = Irssi::server_find_tag('ServerTag')

$server->channel_find('#channel')

Then you can get a list of all the associated nick objects through $chan->nicks() or a specific nick by $chan->nick_find('nick'). You can even find nicknames by searching for a matching host mask, for example:

$chan->nick_find_mask('nick!ident@*.example.com')

The nick object features the same properties as the hash element of our previous Net::IRC code, except that it does not provide the server element.

3.5.4.3 PircBot

PircBot [Hack #35] also maintains an internal user list for each channel it is in, so you don't have to worry about maintaining it manually. If your PircBot is already in the channel #irchacks, you can get an array of user objects for that channel by calling the getUsers method, for example:

User[] users = getUsers("#irchacks");

Each User object contains a getNick method that returns the user's nickname, so to print out all the nicknames in #irchacks, you can loop through each element of the array:

for (int i = 0; i < users.length; i++) {

    User user = users[i];

    String nick = user.getNick( );

    System.out.println(nick);

}

Now that you know how to find other users on IRC, you can spend more time enjoying chatting.

Petr Baudis

    Previous Section  < Day Day Up >  Next Section