File Coverage

blib/lib/Mojo/IRC/UA.pm
Criterion Covered Total %
statement 173 202 85.6
branch 67 92 72.8
condition 23 66 34.8
subroutine 43 53 81.1
pod 9 9 100.0
total 315 422 74.6


line stmt bran cond sub pod time code
1             package Mojo::IRC::UA;
2 11     11   66 use Mojo::Base 'Mojo::IRC';
  11         22  
  11         80  
3              
4 11     11   1713 use Data::Dumper ();
  11         21  
  11         134  
5 11     11   48 use IRC::Utils ();
  11         24  
  11         302  
6 11   50 11   51 use constant DEBUG => $ENV{MOJO_IRC_DEBUG} || 0;
  11         64  
  11         45668  
7              
8             has op_timeout => 10;
9              
10             has _parse_namreply_map => sub {
11             my $self = shift;
12             my @prefix = split /[\(\)]/, $self->server_settings->{prefix} || '(@+)ov';
13             my $i = 0;
14             my %map = map { (substr($prefix[2], $i++, 1), $_) } split //, $prefix[1];
15             my $re = "^([$prefix[2]])";
16              
17             warn "[$self->{debug_key}] : parse_namreply_map=@{[Data::Dumper->new([[$re, \%map]])->Indent(0)->Terse(1)->Dump]}\n"
18             if DEBUG == 2;
19              
20             return [qr{$re}, \%map];
21             };
22              
23             sub channels {
24 1     1 1 9 my ($self, $cb) = @_;
25 1         1 my %channels;
26              
27             return $self->_write_and_wait(
28             Parse::IRC::parse_irc("LIST"),
29             {
30             rpl_listend => {}, # :hybrid8.debian.local 323 superman :End of /LIST
31             rpl_list => sub {
32 2     2   5 my ($self, $msg) = @_;
33 2   50     6 my $topic = $msg->{params}[3] // '';
34 2         10 $topic =~ s!^\[\+[a-z]+\]\s?!!; # remove mode from topic, such as [+nt]
35 2         11 $channels{$msg->{params}[1]} = {n_users => $msg->{params}[2], topic => $topic};
36             },
37             },
38             sub {
39 1     1   2 my ($self, $event, $err, $msg) = @_;
40 1         2 my $n = 0;
41              
42 1 50 0     5 return $self->$cb($err || $msg->{params}[1] || $event, {}) if $event =~ /^err_/;
43 1         5 return $self->$cb('', \%channels);
44             },
45 1         4 );
46             }
47              
48             sub channel_topic {
49 7     7 1 1201 my $cb = pop;
50 7         14 my ($self, $channel, $topic) = @_;
51 7 100 100     26 my $res = length($topic // '') ? {} : undef;
52              
53 7 100       14 if (!$channel) {
54 1     1   6 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot get/set topic without channel name.', {}) });
  1         58  
55 1         48 return $self;
56             }
57 6 100       20 if ($channel =~ /\s/) {
58 1     1   6 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot get/set topic on channel with spaces in name.', {}) });
  1         87  
59 1         56 return $self;
60             }
61              
62             return $self->_write_and_wait(
63             $res ? Parse::IRC::parse_irc("TOPIC $channel :$topic") : Parse::IRC::parse_irc("TOPIC $channel"),
64             {
65             err_chanoprivsneeded => {1 => $channel},
66             err_nochanmodes => {1 => $channel},
67             err_notonchannel => {1 => $channel},
68             rpl_notopic => {1 => $channel},
69             rpl_topic => {1 => $channel}, # :hybrid8.debian.local 332 superman #convos :get cool topic
70             topic => {0 => $channel}, # set
71             },
72             sub {
73 5     5   10 my ($self, $event, $err, $msg) = @_;
74              
75 5 100       16 if ($event eq 'rpl_notopic') {
    100          
    100          
76 1         4 $res->{topic} = '';
77             }
78             elsif ($event eq 'rpl_topic') {
79 1   50     4 $res->{topic} = $msg->{params}[2] // '';
80             }
81             elsif ($event eq 'topic') {
82 1         2 $err = '';
83             }
84             else {
85 2   33     11 $err ||= $msg->{params}[2] || $event;
      33        
86             }
87              
88 5 50       19 return $self->$cb($err, $res) if $res;
89 0         0 return $self->$cb($err);
90             }
91 5 100       29 );
92             }
93              
94             sub channel_users {
95 3     3 1 149 my ($self, $channel, $cb) = @_;
96 3         6 my $users = {};
97              
98 3 100       11 if (!$channel) {
99 1     1   6 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot get users without channel name.', {}) });
  1         85  
100 1         51 return $self;
101             }
102              
103             return $self->_write_and_wait(
104             Parse::IRC::parse_irc("NAMES $channel"),
105             {
106             err_toomanymatches => {1 => $channel},
107             err_nosuchserver => {},
108             rpl_endofnames => {1 => $channel},
109             rpl_namreply => sub {
110 4     4   9 my ($self, $msg) = @_;
111 4 50       29 $self->_parse_namreply($msg, $users) if lc $msg->{params}[2] eq lc $channel;
112             },
113             },
114             sub {
115 2     2   9 my ($self, $event, $err, $msg) = @_;
116 2 50 0     13 $self->$cb($event =~ /^err_/ ? $err || $msg->{params}[2] || $event : '', $users);
117             }
118 2         14 );
119             }
120              
121             sub join_channel {
122 6     6 1 66 my ($self, $command, $cb) = @_;
123 6         27 my ($channel) = split /\s/, $command, 2;
124 6         25 my $info = {topic => '', topic_by => '', users => {}};
125              
126             # err_needmoreparams and will not allow special "JOIN 0"
127              
128 6 100       17 if (!$channel) {
129 1     1   7 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot join without channel name.') });
  1         121  
130 1         83 return $self;
131             }
132              
133             return $self->_write_and_wait(
134             Parse::IRC::parse_irc("JOIN $command"),
135             {
136             err_badchanmask => {1 => $channel},
137             err_badchannelkey => {1 => $channel},
138             err_bannedfromchan => {1 => $channel}, # :hybrid8.debian.local 474 superman #convos :Cannot join channel (+b)
139             err_channelisfull => {1 => $channel},
140             err_inviteonlychan => {1 => $channel},
141             err_nosuchchannel => {1 => $channel}, # :hybrid8.debian.local 403 nick #convos :No such channel
142             err_toomanychannels => {1 => $channel},
143             err_toomanytargets => {1 => $channel},
144             err_unavailresource => {1 => $channel},
145             479 => {1 => $channel}, # Illegal channel name
146             rpl_endofnames => {1 => $channel}, # :hybrid8.debian.local 366 superman #convos :End of /NAMES list.
147             err_linkchannel => sub {
148 1     1   3 my ($self, $msg) = @_;
149 1 50       5 return unless lc $msg->{params}[1] eq lc $channel;
150 1         2 for my $item (values %{$msg->{look_for}}) {
  1         7  
151 15 50 66     55 $item->{1} = $msg->{params}[2] if ref $item eq 'HASH' and $item->{1} and $item->{1} eq $channel;
      33        
152             }
153 1         4 $channel = $msg->{params}[2];
154             },
155             rpl_namreply => sub {
156 3     3   10 my ($self, $msg) = @_;
157 3 50       36 $self->_parse_namreply($msg, $info->{users}) if lc $msg->{params}[2] eq lc $channel;
158             },
159             rpl_topic => sub {
160 3     3   9 my ($self, $msg) = @_;
161 3 50       19 $info->{topic} = $msg->{params}[2] if lc $msg->{params}[1] eq lc $channel;
162             },
163             rpl_topicwhotime => sub {
164 3     3   9 my ($self, $msg) = @_;
165 3 50       19 $info->{topic_by} = $msg->{params}[2] if lc $msg->{params}[1] eq lc $channel;
166             },
167             },
168             sub {
169 5     5   16 my ($self, $event, $err, $msg) = @_;
170 5         12 $info->{name} = $channel;
171 5 100 33     51 $self->$cb($event =~ /^(?:err_|479)/ ? $err || $msg->{params}[2] || $event : '', $info);
172             }
173 5         35 );
174             }
175              
176             sub kick {
177 2     2 1 21 my ($self, $command, $cb) = @_;
178 2         12 my ($target, $user) = $command =~ /(\S+)\s+(.*)/;
179 2         6 my $res = {reason => ''};
180              
181 2   50     5 $target //= '';
182 2   50     5 $user //= '';
183             $self->_write_and_wait(
184             Parse::IRC::parse_irc("KICK $command"),
185             {
186             err_needmoreparams => {},
187             err_nosuchchannel => {1 => $target},
188             err_nosuchnick => {1 => $user},
189             err_badchanmask => {1 => $target},
190             err_chanoprivsneeded => {1 => $target},
191             err_usernotinchannel => {1 => $user},
192             err_notonchannel => {1 => $target},
193             kick => {0 => $target, 1 => $user},
194             },
195             sub {
196 2     2   5 my ($self, $event, $err, $msg) = @_;
197 2         7 my ($nick) = IRC::Utils::parse_user($msg->{prefix});
198 2   50     28 $msg->{params}[2] //= '';
199 2 100       9 $res->{reason} = lc $msg->{params}[2] eq lc $nick ? '' : $msg->{params}[2];
200 2 100 33     25 $self->$cb($event =~ /^err_/ ? $err || $msg->{params}[2] || $event : '', $res);
201             }
202 2         24 );
203             }
204              
205             sub mode {
206 0     0 1 0 my $cb = pop;
207 0         0 my ($self, $mode) = @_;
208 0 0 0     0 return $self->_mode_for_channel($1, $2, $cb) if $mode and $mode =~ /(\S+)\s+(.+)/;
209 0         0 return $self->_mode_for_user($mode, $cb); # get or set
210             }
211              
212             sub nick {
213 18 100   18 1 843 my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
214 18         79 my ($self, $nick) = @_;
215              
216 18 100       48 unless ($cb) {
217 15 100 66     147 return $self->{nick} ||= $self->_build_nick unless defined $nick;
218 2         4 $self->{nick} = $nick;
219 2         4 return $self;
220             }
221              
222 3 100       6 if ($nick) {
223 2 50       4 if ($self->{stream}) {
224             $self->_write_and_wait(
225             Parse::IRC::parse_irc("NICK $nick"),
226             {
227             err_erroneusnickname => {0 => $nick},
228             err_nickcollision => {0 => $nick},
229             err_nicknameinuse => {1 => $nick},
230             err_restricted => {},
231             err_unavailresource => {},
232             nick => {0 => $nick}, # :Superman12923!superman@i.love.debian.org NICK :Supermanx12923
233             },
234             sub {
235 2     2   6 my ($self, $event, $err, $msg) = @_;
236 2 100       6 $self->nick($nick) if $event eq 'nick';
237 2 100 33     13 $self->$cb($event =~ /^err_/ ? $err || $msg->{params}[2] || $event : '');
238             }
239 2         8 );
240             }
241             else {
242 0         0 $self->nick($nick)->$cb('');
243             }
244             }
245             else {
246 1         5 $self->$cb('', $self->nick);
247             }
248              
249 3         15 return $self;
250             }
251              
252             sub part_channel {
253 4     4 1 790 my ($self, $channel, $cb) = @_;
254              
255             # err_needmoreparams
256 4 100       11 if (!$channel) {
257 1     1   8 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot part without channel name.') });
  1         131  
258 1         69 return $self;
259             }
260 3 100       18 if ($channel =~ /\s/) {
261 1     1   9 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot part channel with spaces.') });
  1         92  
262 1         55 return $self;
263             }
264              
265             return $self->_write_and_wait(
266             Parse::IRC::parse_irc("PART $channel"),
267             {
268             err_nosuchchannel => {1 => $channel}, # :hybrid8.debian.local 403 nick #convos :No such channel
269             err_notonchannel => {1 => $channel},
270             479 => {1 => $channel}, # Illegal channel name
271             part => {0 => $channel},
272             },
273             sub {
274 2     2   4 my ($self, $event, $err, $msg) = @_;
275 2 100 33     17 $self->$cb($event =~ /^(?:err_|479)/ ? $err || $msg->{params}[2] || $event : '');
276             }
277 2         7 );
278              
279             }
280              
281             sub whois {
282 2     2 1 111 my ($self, $target, $cb) = @_;
283 2         11 my $info = {channels => {}, name => '', nick => $target, server => '', user => ''};
284              
285 2 100       7 unless ($target) {
286 1     1   6 Mojo::IOLoop->next_tick(sub { $self->$cb('Cannot retrieve whois information without target.', {}) });
  1         76  
287 1         50 return $self;
288             }
289              
290             return $self->_write_and_wait(
291             Parse::IRC::parse_irc("WHOIS $target"),
292             {
293             err_nosuchnick => {1 => $target}, # :hybrid8.debian.local 401 superman batman :No such nick/channel
294             err_nosuchserver => {1 => $target},
295             rpl_away => {1 => $target},
296             rpl_endofwhois => {1 => $target},
297             rpl_whoischannels => sub {
298 1     1   3 my ($self, $msg) = @_;
299 1 50       9 return unless lc $msg->{params}[1] eq lc $target;
300 1   50     10 for (split /\s+/, $msg->{params}[2] || '') {
301 2         21 my ($mode, $channel) = /^([+@]?)(.+)$/;
302 2         12 $info->{channels}{$channel} = {mode => $mode};
303             }
304             },
305             rpl_whoisidle => sub {
306 1     1   8 my ($self, $msg) = @_;
307 1 50       6 return unless lc $msg->{params}[1] eq lc $target;
308 1         7 $info->{idle_for} = 0 + $msg->{params}[2];
309             },
310             rpl_whoisoperator => {}, # TODO
311             rpl_whoisserver => sub {
312 1     1   3 my ($self, $msg) = @_;
313 1 50       5 return unless lc $msg->{params}[1] eq lc $target;
314 1         3 $info->{server} = $msg->{params}[2];
315 1         3 $info->{server_info} = $msg->{params}[3];
316             },
317             rpl_whoisuser => sub {
318 1     1   3 my ($self, $msg) = @_;
319 1 50       6 return unless lc $msg->{params}[1] eq lc $target;
320 1         3 $info->{nick} = $msg->{params}[1];
321 1         2 $info->{user} = $msg->{params}[2];
322 1         3 $info->{host} = $msg->{params}[3];
323 1         2 $info->{name} = $msg->{params}[5];
324             },
325             },
326             sub {
327 1     1   3 my ($self, $event, $err, $msg) = @_;
328 1 50 0     7 $self->$cb($event =~ /^err_/ ? $err || $msg->{params}[2] || $event : '', $info);
329             }
330 1         7 );
331             }
332              
333             sub _dispatch_message {
334 89     89   144 my ($self, $msg) = @_;
335 89   100     339 my $listeners = $self->{write_and_wait}{$msg->{event}} || {};
336              
337 89         262 $self->$_($msg) for values %$listeners;
338 89         666 $self->SUPER::_dispatch_message($msg);
339             }
340              
341             sub _mode_for_channel {
342 0     0   0 my ($self, $target, $mode, $cb) = @_;
343 0         0 my $res = {banlist => [], exceptlist => [], invitelist => [], uniqopis => [], mode => $mode, params => ''};
344              
345             return $self->_write_and_wait(
346             Parse::IRC::parse_irc("MODE $target $mode"),
347             {
348             err_chanoprivsneeded => {1 => $target},
349             err_keyset => {1 => $target},
350             err_needmoreparams => {1 => $target},
351             err_nochanmodes => {1 => $target},
352             err_unknownmode => {1 => $target},
353             err_usernotinchannel => {1 => $target},
354             mode => {0 => $target},
355             rpl_endofbanlist => {1 => $target},
356             rpl_endofexceptlist => {1 => $target},
357             rpl_endofinvitelist => {1 => $target},
358 0     0   0 rpl_channelmodeis => sub { @$res{qw(mode params)} = @{$_[1]->{params}}[1, 2] },
  0         0  
359 0     0   0 rpl_banlist => sub { push @{$res->{banlist}}, $_[1]->{params}[1] },
  0         0  
360 0     0   0 rpl_exceptlist => sub { push @{$res->{exceptlist}}, $_[1]->{params}[1] },
  0         0  
361 0     0   0 rpl_invitelist => sub { push @{$res->{invitelist}}, $_[1]->{params}[1] },
  0         0  
362 0     0   0 rpl_uniqopis => sub { push @{$res->{uniqopis}}, $_[1]->{params}[1] },
  0         0  
363             },
364             sub {
365 0     0   0 my ($self, $event, $err, $msg) = @_;
366 0 0 0     0 $self->$cb($event =~ /^(?:err_)/ ? $err || $msg->{params}[2] || $event : '', $res);
367             }
368 0         0 );
369             }
370              
371             sub _mode_for_user {
372 0     0   0 my ($self, $mode, $cb) = @_;
373 0         0 my $nick = $self->nick;
374              
375             return $self->_write_and_wait(
376             Parse::IRC::parse_irc($mode ? "MODE $nick $mode" : "MODE $nick"),
377             {
378             err_umodeunknownflag => {},
379             err_needmoreparams => {},
380             err_usersdontmatch => {},
381             mode => {0 => $nick},
382             rpl_umodeis => {0 => $nick},
383             },
384             sub {
385 0     0   0 my ($self, $event, $err, $msg) = @_;
386 0 0 0     0 $self->$cb($event =~ /^(?:err_)/ ? $err || $event : '', $msg->{params}[1]);
387             }
388 0 0       0 );
389             }
390              
391             sub _parse_namreply {
392 7     7   16 my ($self, $msg, $users) = @_;
393 7         19 my ($re, $map) = @{$self->_parse_namreply_map};
  7         26  
394              
395 7         69 for my $nick (sort { lc $a cmp lc $b } split /\s+/, $msg->{params}[3]) {
  13         50  
396 18 100 33     158 $users->{$nick}{mode} = $nick =~ s/$re// ? $map->{$1} || $1 : '';
397             }
398             }
399              
400             sub _write_and_wait {
401 20     20   1471 my ($self, $msg, $look_for, $handler) = @_;
402 20         42 my $cmd = $msg->{raw_line};
403 20         35 my ($tid, $timeout);
404              
405             # This method will send a IRC command to the server and wait for a
406             # corresponding IRC event is returned from the server. On such an
407             # event, the $handler callback will be called, but only if the event
408             # received match the rules set in $look_for.
409              
410 20         59 Scalar::Util::weaken($self);
411              
412             # We want a "master timeout" as well, in case the server never send
413             # us any response.
414             $tid = Mojo::IOLoop->timer(
415             ($timeout = $self->op_timeout),
416             sub {
417 1     1   300699 delete $self->{write_and_wait}{$_}{$cmd} for keys %$look_for;
418 1         23 $self->$handler(err_timeout => "Response timeout after ${timeout}s.", {});
419             }
420 20         99 );
421              
422             # Set up which IRC events to look for
423 20         1171 for my $event (keys %$look_for) {
424             $self->{write_and_wait}{$event}{$cmd} = sub {
425 40     40   79 my ($self, $res) = @_;
426 40         63 my $needle = $look_for->{$event};
427 40         73 $res->{look_for} = $look_for;
428 40 100       137 return $self->$needle($res) if ref $needle eq 'CODE';
429              
430 20         73 for my $k (keys %$needle) {
431 20 50       104 my $v = $k =~ /^\d/ ? $res->{params}[$k] : $res->{$k};
432 20 100       112 return unless lc $v eq lc $needle->{$k};
433             }
434              
435 19         110 Mojo::IOLoop->remove($tid);
436 19         927 delete $self->{write_and_wait}{$_}{$cmd} for keys %$look_for;
437 19         53 $self->$handler($event => '', $res);
438 160         703 };
439             }
440              
441             # Write the command to the IRC server and stop looking for events
442             # if the write fails.
443             $self->write(
444             $msg->{raw_line},
445             sub {
446 20     20   67 my ($self, $err) = @_;
447 20 50       67 return unless $err; # no error
448 0         0 Mojo::IOLoop->remove($tid);
449 0         0 delete $self->{write_and_wait}{$_}{$cmd} for keys %$look_for;
450 0         0 $self->$handler(err_write => $err, {});
451             }
452 20         132 );
453              
454 20         48 return $self;
455             }
456              
457             1;
458              
459             =encoding utf8
460              
461             =head1 NAME
462              
463             Mojo::IRC::UA - IRC Client with sugar on top
464              
465             =head1 SYNOPSIS
466              
467             use Mojo::IRC::UA;
468             my $irc = Mojo::IRC::UA->new;
469              
470             =head1 DESCRIPTION
471              
472             L is a module which extends L with methods
473             that can track changes in state on the IRC server.
474              
475             This module is EXPERIMENTAL and can change without warning.
476              
477             =head1 ATTRIBUTES
478              
479             L inherits all attributes from L and implements the
480             following new ones.
481              
482             =head2 op_timeout
483              
484             $int = $self->op_timeout;
485             $self = $self->op_timeout($int);
486              
487             Max number of seconds to wait for a response from the IRC server.
488              
489             =head1 EVENTS
490              
491             L inherits all events from L and implements the
492             following new ones.
493              
494             =head1 METHODS
495              
496             L inherits all methods from L and implements the
497             following new ones.
498              
499             =head2 channels
500              
501             $self = $self->channels(sub { my ($self, $err, $channels) = @_; });
502              
503             Will retrieve available channels on the IRC server. C<$channels> has this
504             structure on success:
505              
506             {
507             "#convos" => {n_users => 4, topic => "[+nt] some cool topic"},
508             }
509              
510             NOTE: This might take a long time, if the server has a lot of channels.
511              
512             =head2 channel_topic
513              
514             $self = $self->channel_topic($channel, $topic, sub { my ($self, $err) = @_; });
515             $self = $self->channel_topic($channel, sub { my ($self, $err, $res) = @_; });
516              
517             Used to get or set topic for a channel. C<$res> is a hash with a key "topic" which
518             holds the current topic.
519              
520             =head2 channel_users
521              
522             $self = $self->channel_users($channel, sub { my ($self, $err, $users) = @_; });
523              
524             This can retrieve the users in a channel. C<$users> contains this structure:
525              
526             {
527             jhthorsen => {mode => "o"},
528             Superman => {mode => ""},
529             }
530              
531             This method is EXPERIMENTAL and can change without warning.
532              
533             =head2 join_channel
534              
535             $self = $self->join_channel($channel => sub { my ($self, $err, $info) = @_; });
536              
537             Used to join an IRC channel. C<$err> will be false (empty string) on a
538             successful join. C<$info> will contain information about the joined channel:
539              
540             {
541             name => "#channel_name",
542             topic => "some cool topic",
543             topic_by => "jhthorsen",
544             users => {
545             jhthorsen => {mode => "@"},
546             Superman => {mode => ""},
547             },
548             }
549              
550             "name" in C<$info> holds the actual channel name that is joined. This will not
551             be the same as C<$channel> in case of "ERR_LINKCHANNEL" (470) events, where you
552             are automatically redirected to another channel.
553              
554             NOTE! This method will fail if the channel is already joined. Unfortunately,
555             the way it will fail is simply by not calling the callback. This should be
556             fixed - Just don't know how yet.
557              
558             =head2 kick
559              
560             $self = $self->kick("#channel superman", sub { my ($self, $err, $res) = @_; });
561              
562             Used to kick a user. C<$res> looks like this:
563              
564             {reason => "you don't behave"}
565              
566             =head2 mode
567              
568             $self = $self->mode(sub { my ($self, $err, $mode) = @_; });
569             $self = $self->mode("-i", sub { my ($self, $err, $mode) = @_; });
570             $self = $self->mode("#channel +k secret", sub { my ($self, $err, $mode) = @_; });
571              
572             This method is used to get or set a user mode or set a channel mode.
573              
574             C<$mode> is EXPERIMENTAL, but holds a hash, with "mode" as key.
575              
576             Note that this method seems to be unstable. Working on a fix:
577             L.
578              
579             =head2 nick
580              
581             $self = $self->nick($nick => sub { my ($self, $err) = @_; });
582             $self = $self->nick(sub { my ($self, $err, $nick) = @_; });
583              
584             Used to set or get the nick for this connection.
585              
586             Setting the nick will change L I the nick is actually
587             changed on the server.
588              
589             =head2 part_channel
590              
591             $self = $self->part_channel($channel => sub { my ($self, $err) = @_; });
592              
593             Used to part/leave a channel.
594              
595             =head2 whois
596              
597             $self = $self->whois($target, sub { my ($self, $err, $info) = @_; });
598              
599             Used to retrieve information about a user. C<$info> contains this information
600             on success:
601              
602             {
603             channels => {"#convos => {mode => "@"}],
604             host => "example.com",
605             idle_for => 17454,
606             name => "Jan Henning Thorsen",
607             nick => "batman",
608             server => "hybrid8.debian.local",
609             user => "jhthorsen",
610             },
611              
612             =head1 COPYRIGHT AND LICENSE
613              
614             Copyright (C) 2014, Jan Henning Thorsen
615              
616             This program is free software, you can redistribute it and/or modify it under
617             the terms of the Artistic License version 2.0.
618              
619             =head1 AUTHOR
620              
621             Jan Henning Thorsen - C
622              
623             =cut