Hack 15 Find Users in Channels


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 +paskyAs 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 NameThe 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 attacksin 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 ( ) {This function checks whether there is anyone on a channel from a
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;
}
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.When you want to fill the %userlist hash, you
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);
}
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 trivialjust 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 objectirssi
passes it to your event hook, or you can get it by doing this:
$server = Irssi::server_find_tag('ServerTag')Then you can get a list of all the associated nick objects through
$server->channel_find('#channel')
$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++) {Now that you know how to find other users on IRC, you can spend more
User user = users[i];
String nick = user.getNick( );
System.out.println(nick);
}
time enjoying chatting. Petr Baudis