File Coverage

blib/lib/Circle/Net/IRC.pm
Criterion Covered Total %
statement 111 712 15.5
branch 0 218 0.0
condition 0 69 0.0
subroutine 37 141 26.2
pod 1 76 1.3
total 149 1216 12.2


line stmt bran cond sub pod time code
1             # You may distribute under the terms of the GNU General Public License
2             #
3             # (C) Paul Evans, 2008-2014 -- leonerd@leonerd.org.uk
4              
5             package Circle::Net::IRC;
6              
7 4     4   16 use strict;
  4         6  
  4         102  
8 4     4   12 use warnings;
  4         4  
  4         105  
9              
10 4     4   12 use base qw( Circle::Net Circle::Ruleable );
  4         5  
  4         1146  
11             __PACKAGE__->APPLY_Ruleable;
12              
13 4     4   16 use base qw( Circle::Rule::Store ); # for the attributes
  4         6  
  4         250  
14              
15 4     4   16 use constant NETTYPE => 'irc';
  4         3  
  4         232  
16              
17 4     4   1563 use Circle::Net::IRC::Channel;
  4         12  
  4         150  
18 4     4   1920 use Circle::Net::IRC::User;
  4         10  
  4         117  
19              
20 4     4   28 use Circle::TaggedString;
  4         6  
  4         105  
21              
22 4     4   16 use Circle::Rule::Store;
  4         6  
  4         91  
23              
24 4     4   17 use Circle::Widget::Box;
  4         19  
  4         90  
25 4     4   15 use Circle::Widget::Label;
  4         7  
  4         134  
26              
27 4     4   2127 use Net::Async::IRC 0.10; # on_irc_error
  4         51201  
  4         127  
28 4     4   27 use IO::Async::Timer::Countdown;
  4         4  
  4         80  
29              
30 4     4   13 use Text::Balanced qw( extract_delimited );
  4         8  
  4         181  
31 4     4   20 use Scalar::Util qw( weaken );
  4         6  
  4         7983  
32              
33             sub new
34             {
35 0     0 0   my $class = shift;
36 0           my %args = @_;
37              
38 0           my $self = $class->SUPER::new( %args );
39              
40 0           $self->{root} = $args{root};
41 0           my $loop = $self->{loop} = $args{loop};
42              
43             # For WindowItem
44 0           $self->set_prop_tag( $args{tag} );
45              
46             my $irc = $self->{irc} = Net::Async::IRC->new(
47             # TODO: All these event handler subs should be weaselled
48             on_message => sub {
49 0     0     my ( $irc, $command, $message, $hints ) = @_;
50 0           $self->on_message( $command, $message, $hints );
51             },
52              
53             on_closed => sub {
54 0     0     $self->on_closed;
55             },
56              
57             on_irc_error => sub {
58 0     0     my ( $irc, $message ) = @_;
59 0           $self->push_displayevent( "status", { text => "IRC error $message" } );
60 0           $self->close_now;
61             },
62              
63             encoding => "UTF-8",
64              
65             pingtime => 120,
66             on_ping_timeout => sub {
67 0     0     $self->on_ping_timeout;
68             },
69              
70             pongtime => 60,
71             on_pong_reply => sub {
72 0     0     my ( $irc, $lag ) = @_;
73 0           $self->on_ping_reply( $lag );
74             },
75 0           );
76              
77 0           weaken( my $weakself = $self );
78             $self->{reconnect_timer} = IO::Async::Timer::Countdown->new(
79             delay => 1, # Doesn't matter, as ->enqueue_reconnect will set it before start anyway
80 0 0   0     on_expire => sub { $weakself and $weakself->reconnect },
81 0           );
82 0           $loop->add( $self->{reconnect_timer} );
83              
84 0           $self->{servers} = [];
85              
86 0           $self->{channels} = {};
87 0           $self->{users} = {};
88              
89 0           my $rulestore = $self->init_rulestore( parent => $args{root}->{rulestore} );
90              
91 0           $rulestore->register_cond( matchnick => $self );
92 0           $rulestore->register_cond( fromnick => $self );
93 0           $rulestore->register_cond( channel => $self );
94 0           $rulestore->register_cond( isaction => $self );
95              
96 0           $rulestore->register_action( highlight => $self );
97 0           $rulestore->register_action( display => $self );
98 0           $rulestore->register_action( chaction => $self );
99              
100 0           $rulestore->new_chain( "input" );
101              
102 0           $rulestore->get_chain( "input" )->append_rule( "matchnick: highlight" );
103              
104 0           $rulestore->new_chain( "output" );
105              
106 0           $self->set_network_status( "disconnected" );
107              
108 0           return $self;
109             }
110              
111             sub describe
112             {
113 0     0 1   my $self = shift;
114 0           return __PACKAGE__."[". $self->get_prop_tag . "]";
115             }
116              
117             sub get_prop_users
118             {
119 0     0 0   my $self = shift;
120              
121 0           my $users = $self->{users};
122 0           return [ values %$users ];
123             }
124              
125             sub reify
126       0 0   {
127             # always real; this is a no-op
128             }
129              
130             sub get_channel_if_exists
131             {
132 0     0 0   my $self = shift;
133 0           my ( $channame ) = @_;
134              
135 0           my $irc = $self->{irc};
136 0           my $channame_folded = $irc->casefold_name( $channame );
137              
138 0           return $self->{channels}->{$channame_folded};
139             }
140              
141             sub get_channel_or_create
142             {
143 0     0 0   my $self = shift;
144 0           my ( $channame ) = @_;
145              
146 0           my $irc = $self->{irc};
147 0           my $channame_folded = $irc->casefold_name( $channame );
148              
149 0 0         return $self->{channels}->{$channame_folded} if exists $self->{channels}->{$channame_folded};
150              
151 0           my $registry = $self->{registry};
152             my $chanobj = $registry->construct(
153             "Circle::Net::IRC::Channel",
154             root => $self->{root},
155 0           net => $self,
156             irc => $irc,
157             name => $channame,
158             );
159              
160 0           my $root = $self->{root};
161              
162 0           $self->{channels}->{$channame_folded} = $chanobj;
163             $chanobj->subscribe_event( destroy => sub {
164 0     0     my ( $chanobj ) = @_;
165 0           $root->broadcast_sessions( "delete_item", $chanobj );
166 0           $self->del_prop_channels( $chanobj );
167 0           delete $self->{channels}->{$channame_folded};
168 0           } );
169              
170 0           $self->add_prop_channels( $chanobj );
171              
172 0           return $chanobj;
173             }
174              
175             sub get_user_if_exists
176             {
177 0     0 0   my $self = shift;
178 0           my ( $nick ) = @_;
179              
180 0           my $irc = $self->{irc};
181 0           my $nick_folded = $irc->casefold_name( $nick );
182              
183 0           return $self->{users}->{$nick_folded};
184             }
185              
186             sub get_user_or_create
187             {
188 0     0 0   my $self = shift;
189 0           my ( $nick ) = @_;
190              
191 0 0 0       unless( defined $nick and length $nick ) {
192 0           warn "Unable to create a new user with an empty nick\n";
193 0           return undef;
194             }
195              
196 0           my $irc = $self->{irc};
197 0           my $nick_folded = $irc->casefold_name( $nick );
198              
199 0 0         return $self->{users}->{$nick_folded} if exists $self->{users}->{$nick_folded};
200              
201 0           my $registry = $self->{registry};
202             my $userobj = $registry->construct(
203             "Circle::Net::IRC::User",
204             root => $self->{root},
205 0           net => $self,
206             irc => $irc,
207             name => $nick,
208             );
209              
210 0           my $root = $self->{root};
211              
212 0           $self->{users}->{$nick_folded} = $userobj;
213              
214             $userobj->subscribe_event( destroy => sub {
215 0     0     my ( $userobj ) = @_;
216 0           $root->broadcast_sessions( "delete_item", $userobj );
217 0           $self->del_prop_users( $userobj );
218 0           my $nick_folded = $irc->casefold_name( $userobj->get_prop_name );
219 0           delete $self->{users}->{$nick_folded};
220 0           } );
221              
222             $userobj->subscribe_event( change_nick => sub {
223 0     0     my ( undef, $oldnick, $oldnick_folded, $newnick, $newnick_folded ) = @_;
224 0           $self->{users}->{$newnick_folded} = delete $self->{users}->{$oldnick_folded};
225 0           } );
226              
227 0           $self->add_prop_users( $userobj );
228              
229 0           return $userobj;
230             }
231              
232             sub get_target_if_exists
233             {
234 0     0 0   my $self = shift;
235 0           my ( $name ) = @_;
236              
237 0           my $irc = $self->{irc};
238 0           my $type = $irc->classify_name( $name );
239              
240 0 0         if( $type eq "channel" ) {
    0          
241 0           return $self->get_channel_if_exists( $name );
242             }
243             elsif( $type eq "user" ) {
244 0           return $self->get_user_if_exists( $name );
245             }
246             else {
247 0           return undef;
248             }
249             }
250              
251             sub get_target_or_create
252             {
253 0     0 0   my $self = shift;
254 0           my ( $name ) = @_;
255              
256 0           my $irc = $self->{irc};
257 0           my $type = $irc->classify_name( $name );
258              
259 0 0         if( $type eq "channel" ) {
    0          
260 0           return $self->get_channel_or_create( $name );
261             }
262             elsif( $type eq "user" ) {
263 0           return $self->get_user_or_create( $name );
264             }
265             else {
266 0           return undef;
267             }
268             }
269              
270             sub connect
271             {
272 0     0 0   my $self = shift;
273 0           my %args = @_;
274              
275 0           my $irc = $self->{irc};
276              
277 0           my $host = $args{host};
278 0   0       my $nick = $args{nick} || $self->get_prop_nick || $self->{configured_nick};
279              
280 0 0 0       if( $args{SSL} and not eval { require IO::Async::SSL } ) {
  0            
281 0           return Future->new->fail( "SSL is set but IO::Async::SSL is not available" );
282             }
283              
284 0 0         $self->{loop}->add( $irc ) if !$irc->loop;
285             my $f = $irc->login(
286             host => $host,
287             service => $args{port},
288             nick => $nick,
289             user => $args{ident},
290             pass => $args{pass},
291              
292             ( $args{SSL} ? (
293             extensions => [qw( SSL )],
294             SSL_verify_mode => IO::Socket::SSL::SSL_VERIFY_NONE(),
295             ) : () ),
296              
297             local_host => $args{local_host} || $self->{local_host},
298              
299             on_login => sub {
300 0     0     foreach my $target ( values %{ $self->{channels} }, values %{ $self->{users} } ) {
  0            
  0            
301 0           $target->on_connected;
302             }
303              
304 0           $self->set_prop_nick( $nick );
305              
306 0           $self->set_network_status( "" );
307              
308 0           $self->fire_event( "connected" );
309             },
310              
311             on_error => $args{on_error},
312 0 0 0       );
313              
314 0           $self->set_network_status( "connecting" );
315              
316 0     0     $f->on_fail( sub { $self->set_network_status( "disconnected" ) } );
  0            
317              
318 0           return $f;
319             }
320              
321             sub connected
322             {
323 0     0 0   my $self = shift;
324              
325             # Consider we're "connected" if the underlying IRC object is logged in
326 0           my $irc = $self->{irc};
327 0           return $irc->is_loggedin;
328             }
329              
330             # Map mIRC's colours onto an approximation of ANSI terminal
331             my @irc_colour_map = (
332             15, 0, 4, 2, # white black blue green
333             9, 1, 5, 3, # red [brown=darkred] [purple=darkmagenta] [orange=darkyellow]
334             11, 10, 6, 14, # yellow lightgreen cyan lightcyan
335             12, 13, 8, 7 # lightblue [pink=magenta] grey lightgrey
336             );
337              
338             sub format_colour
339             {
340 0     0 0   my $self = shift;
341 0           my ( $colcode ) = @_;
342              
343 0 0         return $colcode if $colcode =~ m/^#[0-9a-f]{6}/i;
344 0 0         return "#$1$1$2$2$3$3" if $colcode =~ m/^#([0-9a-f])([0-9a-f])([0-9a-f])/i;
345              
346 0 0 0       return sprintf( "ansi.col%02d", $irc_colour_map[$1] ) if $colcode =~ m/^(\d\d?)/ and defined $irc_colour_map[$1];
347              
348 0           return undef;
349             }
350              
351             sub format_text_tagged
352             {
353 0     0 0   my $self = shift;
354 0           my ( $text ) = @_;
355              
356             # IRC [well, technically mIRC but other clients have adopted it] uses Ctrl
357             # characters to toggle formatting
358             # ^B = bold
359             # ^U = underline
360             # ^_ = underline
361             # ^R = reverse or italic - we'll use italic
362             # ^V = reverse
363             # ^] = italics
364             # ^O = reset
365             # ^C = colour; followed by a code
366             # ^C = reset colours
367             # ^Cff = foreground
368             # ^Cff,bb = background
369             #
370             # irssi uses the following
371             # ^D$$ = foreground/background, in chr('0'+$colour),
372             # ^Db = underline
373             # ^Dc = bold
374             # ^Dd = reverse or italic - we'll use italic
375             # ^Dg = reset colours
376             #
377             # As a side effect we'll also strip all the other Ctrl chars
378              
379             # We'll also look for "poor-man's" highlighting
380             # *bold*
381             # _underline_
382             # /italic/
383              
384 0           my $ret = Circle::TaggedString->new();
385              
386 0           my %format;
387              
388 0           while( length $text ) {
389 0 0         if( $text =~ s/^([\x00-\x1f])// ) {
390 0           my $ctrl = chr(ord($1)+0x40);
391              
392 0 0 0       if( $ctrl eq "B" ) {
    0 0        
    0          
    0          
    0          
    0          
    0          
393 0 0         $format{b} ? delete $format{b} : ( $format{b} = 1 );
394             }
395             elsif( $ctrl eq "U" or $ctrl eq "_" ) {
396 0 0         $format{u} ? delete $format{u} : ( $format{u} = 1 );
397             }
398             elsif( $ctrl eq "R" or $ctrl eq "]" ) {
399 0 0         $format{i} ? delete $format{i} : ( $format{i} = 1 );
400             }
401             elsif( $ctrl eq "V" ) {
402 0 0         $format{rv} ? delete $format{rv} : ( $format{rv} = 1 );
403             }
404             elsif( $ctrl eq "O" ) {
405 0           undef %format;
406             }
407             elsif( $ctrl eq "C" ) {
408 0           my $colourre = qr/#[0-9a-f]{6}|#[0-9a-f]{3}|\d\d?/i;
409              
410 0 0         if( $text =~ s/^($colourre),($colourre)// ) {
    0          
411 0 0         $format{fg} = $self->format_colour( $1 ) if $self->{use_mirc_colours};
412 0 0         $format{bg} = $self->format_colour( $2 ) if $self->{use_mirc_colours};
413             }
414             elsif( $text =~ s/^($colourre)// ) {
415 0 0         $format{fg} = $self->format_colour( $1 ) if $self->{use_mirc_colours};
416             }
417             else {
418 0           delete $format{fg};
419 0           delete $format{bg};
420             }
421             }
422             elsif( $ctrl eq "D" ) {
423 0 0         if( $text =~ s/^b// ) { # underline
    0          
    0          
    0          
424 0 0         $format{u} ? delete $format{u} : ( $format{u} = 1 );
425             }
426             elsif( $text =~ s/^c// ) { # bold
427 0 0         $format{b} ? delete $format{b} : ( $format{b} = 1 );
428             }
429             elsif( $text =~ s/^d// ) { # revserse/italic
430 0 0         $format{i} ? delete $format{i} : ( $format{i} = 1 );
431             }
432             elsif( $text =~ s/^g// ) {
433 0           undef %format
434             }
435             else {
436 0           $text =~ s/^(.)(.)//;
437 0           my ( $fg, $bg ) = map { ord( $_ ) - ord('0') } ( $1, $2 );
  0            
438 0 0         if( $fg > 0 ) {
439 0           $format{fg} = sprintf( "ansi.col%02d", $fg );
440             }
441 0 0         if( $bg > 0 ) {
442 0           $format{bg} = sprintf( "ansi.col%02d", $bg );
443             }
444             }
445             }
446             else {
447 0           print STDERR "Unhandled Ctrl code ^$ctrl\n";
448             }
449             }
450             else {
451 0           $text =~ s/^([^\x00-\x1f]+)//;
452 0           my $piece = $1;
453              
454             # Now scan this piece for the text-based ones
455 0           while( length $piece ) {
456             # Look behind/ahead asserts to ensure we don't capture e.g.
457             # /usr/bin/perl by mistake
458 0 0         if( $piece =~ s/^(.*?)(?
459 0           my ( $pre, $inner, $type ) = ( $1, $2, $3 );
460              
461 0 0         $ret->append_tagged( $pre, %format ) if length $pre;
462              
463 0           my %innerformat = %format;
464              
465 0           $type =~ tr{*_/}{bui};
466 0           $innerformat{$type} = 1;
467              
468 0           $ret->append_tagged( $inner, %innerformat );
469             }
470             else {
471 0           $ret->append_tagged( $piece, %format );
472 0           $piece = "";
473             }
474             }
475             }
476             }
477              
478 0           return $ret;
479             }
480              
481             sub format_text
482             {
483 0     0 0   my $self = shift;
484 0           my ( $text ) = @_;
485              
486 0           return $self->format_text_tagged( $text );
487             }
488              
489             ###
490             # Rule subs
491             ###
492              
493             sub parse_cond_matchnick
494             : Rule_description("Look for my IRC nick in the text")
495             : Rule_format('')
496             {
497 0     0 0 0 my $self = shift;
498 0         0 return;
499 4     4   28 }
  4         5  
  4         34  
500              
501             sub deparse_cond_matchnick
502             {
503 0     0 0   my $self = shift;
504 0           return;
505             }
506              
507             sub eval_cond_matchnick
508             {
509 0     0 0   my $self = shift;
510 0           my ( $event, $results ) = @_;
511              
512 0           my $text = $event->{text}->str;
513              
514 0           my $nick = $self->{irc}->nick;
515              
516 0           pos( $text ) = 0;
517              
518 0           my $matched;
519              
520 0           while( $text =~ m/(\Q$nick\E)/gi ) {
521 0           my ( $start, $end ) = ( $-[0], $+[0] );
522 0           my $len = $end - $start;
523              
524 0           $results->push_result( "matchgroups", [ [ $start, $len ] ] );
525 0           $matched = 1;
526             }
527              
528 0           return $matched;
529             }
530              
531             sub parse_cond_fromnick
532             : Rule_description("Match the message originating nick against a regexp or string")
533             : Rule_format('/regexp/ or "literal"')
534             {
535 0     0 0 0 my $self = shift;
536 0         0 my ( $spec ) = @_;
537              
538 0 0       0 if( $spec =~ m/^"/ ) {
    0          
539             # Literal
540 0         0 my $nick = extract_delimited( $spec, q{"} );
541 0         0 s/^"//, s/"$// for $nick;
542              
543 0         0 return literal => $nick;
544             }
545             elsif( $spec =~ m{^/} ) {
546             # Regexp
547 0         0 my $re = extract_delimited( $spec, q{/} );
548 0         0 s{^/}{}, s{/$}{} for $re;
549              
550 0 0       0 my $iflag = 1 if $spec =~ s/^i//;
551              
552 0 0       0 return re => qr/$re/i if $iflag;
553 0         0 return re => qr/$re/;
554             }
555 4     4   2029 }
  4         6  
  4         17  
556              
557             sub deparse_cond_fromnick
558             {
559 0     0 0   my $self = shift;
560 0           my ( $type, $pattern ) = @_;
561              
562 0 0         if( $type eq "literal" ) {
    0          
563 0           return qq{"$pattern"};
564             }
565             elsif( $type eq "re" ) {
566             # Perl tries to put (?-ixsm:RE) around our pattern. Lets attempt to remove
567             # it if we can
568 0 0         return "/$1/" if $pattern =~ m/^\(\?-xism:(.*)\)$/;
569 0 0         return "/$1/i" if $pattern =~ m/^\(\?i-xsm:(.*)\)$/;
570              
571             # Failed. Lets just be safe then
572 0           return "/$pattern/";
573             }
574             }
575              
576             sub eval_cond_fromnick
577             {
578 0     0 0   my $self = shift;
579 0           my ( $event, $results, $type, $pattern ) = @_;
580              
581 0           my $src = $event->{prefix_name_folded};
582              
583 0 0         if( $type eq "literal" ) {
    0          
584 0           my $irc = $self->{irc};
585              
586 0           return $src eq $irc->casefold_name( $pattern );
587             }
588             elsif( $type eq "re" ) {
589 0           return $src =~ $pattern;
590             }
591             }
592              
593             sub parse_cond_channel
594             : Rule_description("Event comes from a (named) channel")
595             : Rule_format('"name"?')
596             {
597 0     0 0 0 my $self = shift;
598 0         0 my ( $spec ) = @_;
599              
600 0 0 0     0 if( defined $spec and $spec =~ m/^"/ ) {
601 0         0 my $name = extract_delimited( $spec, q{"} );
602 0         0 s/^"//, s/"$// for $name;
603              
604 0         0 return $name;
605             }
606              
607 0         0 return undef;
608 4     4   1693 }
  4         10  
  4         15  
609              
610             sub deparse_cond_channel
611             {
612 0     0 0   my $self = shift;
613 0           my ( $name ) = @_;
614              
615 0 0         return qq{"$name"} if defined $name;
616 0           return;
617             }
618              
619             sub eval_cond_channel
620             {
621 0     0 0   my $self = shift;
622 0           my ( $event, $results, $name ) = @_;
623              
624 0 0 0       return 0 unless ( $event->{target_type} || "" ) eq "channel";
625              
626 0 0         return 1 unless defined $name;
627              
628 0           my $irc = $self->{irc};
629 0           return $event->{target_name_folded} eq $irc->casefold_name( $name );
630             }
631              
632             sub parse_cond_isaction
633             : Rule_description("Event is a CTCP ACTION")
634             : Rule_format('')
635             {
636 0     0 0 0 my $self = shift;
637 0         0 return undef;
638 4     4   1024 }
  4         7  
  4         15  
639              
640             sub deparse_cond_isaction
641             {
642 0     0 0   my $self = shift;
643 0           return;
644             }
645              
646             sub eval_cond_isaction
647             {
648 0     0 0   my $self = shift;
649 0           my ( $event, $results, $name ) = @_;
650              
651 0           return $event->{is_action};
652             }
653              
654             sub parse_action_highlight
655             : Rule_description("Highlight matched regions and set activity level to 3")
656             : Rule_format('')
657             {
658 0     0 0 0 my $self = shift;
659 0         0 return;
660 4     4   843 }
  4         7  
  4         16  
661              
662             sub deparse_action_highlight
663             {
664 0     0 0   my $self = shift;
665 0           return;
666             }
667              
668             sub eval_action_highlight
669             {
670 0     0 0   my $self = shift;
671 0           my ( $event, $results ) = @_;
672              
673 0           my $str = $event->{text};
674              
675 0           foreach my $matchgroup ( @{ $results->get_result( "matchgroups" ) } ) {
  0            
676 0           my ( $start, $len ) = @{$matchgroup->[0]}[0,1];
  0            
677              
678 0           $str->apply_tag( $start, $len, b => 1 );
679 0           $str->apply_tag( $start, $len, fg => "highlight" );
680             }
681              
682 0           $event->{level} = 3;
683             }
684              
685             sub parse_action_display
686             : Rule_description("Set the display window to display an event")
687             : Rule_format('self|server')
688             {
689 0     0 0 0 my $self = shift;
690 0         0 my ( $spec ) = @_;
691              
692 0 0       0 if( $spec eq "self" ) {
    0          
693 0         0 return "self";
694             }
695             elsif( $spec eq "server" ) {
696 0         0 return "server";
697             }
698             else {
699 0         0 die "Unrecognised display spec\n";
700             }
701 4     4   1158 }
  4         8  
  4         14  
702              
703             sub deparse_action_display
704             {
705 0     0 0   my $self = shift;
706 0           my ( $display ) = @_;
707              
708 0           return $display;
709             }
710              
711             sub eval_action_display
712             {
713 0     0 0   my $self = shift;
714 0           my ( $event, $results, $display ) = @_;
715              
716 0           $event->{display} = $display;
717             }
718              
719             sub parse_action_chaction
720             : Rule_description("Change an event to or from being a CTCP ACTION")
721             : Rule_format('0|1')
722             {
723 0     0 0 0 my $self = shift;
724 0         0 my ( $spec ) = @_;
725              
726 0         0 return !!$spec;
727 4     4   859 }
  4         5  
  4         16  
728              
729             sub deparse_action_chaction
730             {
731 0     0 0   my $self = shift;
732 0           my ( $action ) = @_;
733              
734 0           return $action;
735             }
736              
737             sub eval_action_chaction
738             {
739 0     0 0   my $self = shift;
740 0           my ( $event, $results, $action ) = @_;
741              
742 0           $event->{is_action} = $action;
743             }
744              
745             ###
746             # IRC message handlers
747             ###
748              
749             sub on_message
750             {
751 0     0 0   my $self = shift;
752 0           my ( $command, $message, $hints ) = @_;
753              
754 0 0         if( defined $hints->{target_name} ) {
    0          
    0          
755 0           my $target;
756              
757 0 0 0       if( $hints->{target_type} eq "channel" ) {
    0 0        
    0          
758 0           $target = $self->get_channel_or_create( $hints->{target_name} );
759             }
760             elsif( $hints->{target_is_me} and
761             defined $hints->{prefix_name} and
762             not $hints->{prefix_is_me} ) {
763             # Handle PRIVMSG and similar from the user
764 0           $target = $self->get_user_or_create( $hints->{prefix_name} );
765             }
766             elsif( $hints->{target_type} eq "user" ) {
767             # Handle numerics about the user - Net::Async::IRC has filled in the target
768 0           $target = $self->get_user_or_create( $hints->{target_name} );
769             }
770              
771 0 0         if( $target ) {
772 0 0         return 1 if $target->on_message( $command, $message, $hints );
773             }
774             }
775 0           elsif( grep { $command eq $_ } qw( NICK QUIT ) ) {
776             # Target all of them
777 0           my $handled = 0;
778              
779 0           my $method = "on_message_$command";
780              
781 0 0 0       $handled = 1 if $self->can( $method ) and $self->$method( $message, $hints );
782              
783 0           foreach my $target ( values %{ $self->{channels} } ) {
  0            
784 0 0         $handled = 1 if $target->$method( $message, $hints );
785             }
786              
787 0           my $nick_folded = $hints->{prefix_nick_folded};
788              
789 0 0         if( my $userobj = $self->get_user_if_exists( $hints->{prefix_nick} ) ) {
790 0 0         $handled = 1 if $userobj->$method( $message, $hints );
791             }
792              
793 0 0         return 1 if $handled;
794             }
795             elsif( $self->can( "on_message_$command" ) ) {
796 0           my $method = "on_message_$command";
797 0           my $handled = $self->$method( $message, $hints );
798              
799 0 0         return 1 if $handled;
800             }
801              
802 0 0 0       if( not $hints->{handled} and not $hints->{synthesized} ) {
803             $self->push_displayevent( "irc.irc", {
804             command => $command,
805             prefix => $message->prefix,
806 0           args => join( " ", map { "'$_'" } $message->args ),
  0            
807             } );
808 0           $self->bump_level( 1 );
809             }
810             }
811              
812             sub on_message_NICK
813             {
814 0     0 0   my $self = shift;
815 0           my ( $message, $hints ) = @_;
816              
817 0 0         if( $hints->{prefix_is_me} ) {
818 0           $self->set_prop_nick( $hints->{new_nick} );
819             }
820              
821 0           return 1;
822             }
823              
824             sub on_message_motd
825             {
826 0     0 0   my $self = shift;
827 0           my ( $message, $hints ) = @_;
828              
829 0           my $motd = $hints->{motd};
830 0           $self->push_displayevent( "irc.motd", { text => $self->format_text($_) } ) for @$motd;
831 0           $self->bump_level( 1 );
832              
833 0           return 1;
834             }
835              
836             sub on_message_RPL_UNAWAY
837             {
838 0     0 0   my $self = shift;
839 0           my ( $message, $hints ) = @_;
840              
841 0           $self->set_prop_away( 0 );
842              
843 0           $self->push_displayevent( "irc.text", { server => $hints->{prefix_host}, text => $hints->{text} } );
844 0           $self->bump_level( 1 );
845              
846 0           return 1;
847             }
848              
849             sub on_message_RPL_NOWAWAY
850             {
851 0     0 0   my $self = shift;
852 0           my ( $message, $hints ) = @_;
853              
854 0           $self->set_prop_away( 1 );
855              
856 0           $self->push_displayevent( "irc.text", { server => $hints->{prefix_host}, text => $hints->{text} } );
857 0           $self->bump_level( 1 );
858              
859 0           return 1;
860             }
861              
862             sub on_message_whois
863             {
864 0     0 0   my $self = shift;
865 0           my ( $message, $hints ) = @_;
866              
867             my $f = delete $self->{whois_gate_f}{$hints->{target_name_folded}}
868 0 0         or return 1;
869              
870 0           $f->done( $hints->{whois} );
871             }
872              
873             sub on_closed
874             {
875 0     0 0   my $self = shift;
876 0           my ( $message ) = @_;
877              
878 0   0       $message ||= "Server is disconnected";
879 0           $self->set_network_status( "disconnected" );
880              
881 0           $self->push_displayevent( "status", { text => $message } );
882              
883 0           foreach my $target ( values %{ $self->{channels} }, values %{ $self->{users} } ) {
  0            
  0            
884 0           $target->on_disconnected( $message );
885             }
886              
887 0           $self->fire_event( "disconnected" );
888              
889 0 0         unless( $self->{no_reconnect_on_close} ) {
890 0           $self->{reconnect_delay_idx} = 0;
891 0           $self->{reconnect_host_idx} = 0;
892 0 0         $self->enqueue_reconnect if !$self->{reconnect_timer}->is_running;
893             }
894 0           undef $self->{no_reconnect_on_close};
895             }
896              
897             my @reconnect_delays = ( 5, 5, 10, 30, 60 );
898             sub enqueue_reconnect
899             {
900 0     0 0   my $self = shift;
901 0   0       my $delay = $reconnect_delays[ $self->{reconnect_delay_idx}++ ] // $reconnect_delays[-1];
902              
903 0           my $timer = $self->{reconnect_timer};
904 0           $timer->configure( delay => $delay );
905 0           $timer->start;
906              
907 0           $self->set_network_status( "reconnect pending..." );
908             }
909              
910             sub reconnect
911             {
912 0     0 0   my $self = shift;
913              
914 0           my $s = $self->{servers}->[ $self->{reconnect_host_idx}++ ];
915 0           $self->{reconnect_host_idx} %= @{ $self->{servers} };
  0            
916              
917             my $f = $self->connect(
918             host => $s->{host},
919             port => $s->{port},
920             user => $s->{user},
921             pass => $s->{pass},
922             SSL => $s->{SSL},
923 0           );
924              
925 0     0     $f->on_fail( sub { $self->enqueue_reconnect } );
  0            
926             }
927              
928             sub on_ping_timeout
929             {
930 0     0 0   my $self = shift;
931              
932 0           $self->on_closed( "Ping timeout" );
933 0           $self->{irc}->close;
934             }
935              
936             sub on_ping_reply
937             {
938 0     0 0   my $self = shift;
939 0           my ( $lag ) = @_;
940              
941 0 0         if( $lag > 1 ) {
942 0           $self->set_network_status( sprintf "lag:%.2f", $lag );
943             }
944             else {
945 0           $self->set_network_status( "" );
946             }
947             }
948              
949             sub method_get_isupport
950             {
951 0     0 0   my $self = shift;
952 0           my ( $ctx, $key ) = @_;
953              
954 0           my $irc = $self->{irc};
955 0           return $irc->isupport( $key );
956             }
957              
958             sub do_join
959             {
960 0     0 0   my $self = shift;
961 0           my ( $channel, $key ) = @_;
962              
963 0   0       my $pending = $self->{pending_joins} //= [];
964              
965 0 0         if( !@$pending ) {
966 0           my $irc = $self->{irc};
967             $self->{loop}->later( sub {
968 0     0     my $channels = join ",", map { $_->[0] } @$pending;
  0            
969 0 0         my $keys = join ",", map { defined $_->[1] ? ( $_->[1] ) : () } @$pending;
  0            
970              
971 0 0         $irc->send_message( "JOIN", undef, $channels, length $keys ? ( $keys ) : () );
972              
973 0           @$pending = ();
974 0           });
975             }
976              
977             # Enqueue keyed joins first, others last
978 0 0         if( defined $key ) {
979 0           unshift @$pending, [ $channel, $key ];
980             }
981             else {
982 0           push @$pending, [ $channel ];
983             }
984             }
985              
986             use Circle::Collection
987             name => 'servers',
988             storage => 'array',
989             attrs => [
990             host => { desc => "hostname" },
991             port => { desc => "alternative port",
992 0 0       0 show => sub { $_ || "6667" },
993             },
994             SSL => { desc => "use SSL",
995 0 0       0 show => sub { $_ ? "SSL" : "" },
996             },
997             ident => { desc => "alternative ident",
998 0 0       0 show => sub { $_ || '$USER' },
999             },
1000             pass => { desc => "connection password",
1001 0 0       0 show => sub { $_ ? "set" : "" },
1002             },
1003 4         87 ],
1004 4     4   5160 ;
  4         5  
1005              
1006             sub command_nick
1007             : Command_description("Change nick")
1008             : Command_arg('nick?')
1009             {
1010 0     0 0 0 my $self = shift;
1011 0         0 my ( $newnick ) = @_;
1012              
1013 0         0 my $irc = $self->{irc};
1014              
1015 0 0       0 if( defined $newnick ) {
1016 0         0 $irc->change_nick( $newnick );
1017 0         0 $self->set_prop_nick( $newnick );
1018             }
1019              
1020 0         0 return;
1021 4     4   1158 }
  4         8  
  4         17  
1022              
1023             sub command_connect
1024             : Command_description("Connect to an IRC server")
1025             : Command_arg('host?')
1026             : Command_opt('port=$', desc => "alternative port (default '6667')")
1027             : Command_opt('SSL=+', desc => "use SSL")
1028             : Command_opt('nick=$', desc => "initial nick")
1029             : Command_opt('ident=$', desc => "alternative ident (default '\$USER')")
1030             : Command_opt('pass=$', desc => "connection password")
1031             : Command_opt('local_host=$', desc => "local host to bind")
1032             {
1033 0     0 0 0 my $self = shift;
1034 0         0 my ( $host, $opts, $cinv ) = @_;
1035              
1036 0         0 my $s;
1037              
1038 0 0       0 if( !defined $host ) {
1039 0 0       0 if( !@{ $self->{servers} } ) {
  0         0  
1040 0         0 $cinv->responderr( "Cannot connect - no servers defined" );
1041 0         0 return;
1042             }
1043              
1044             # TODO: Pick one - for now just the first
1045 0         0 $s = $self->{servers}->[0];
1046              
1047 0         0 $host = $s->{host};
1048             }
1049             else {
1050 0         0 ( $s ) = grep { $_->{host} eq $host } @{ $self->{servers} };
  0         0  
  0         0  
1051 0 0       0 $s or return $cinv->responderr( "No definition for $host" );
1052             }
1053              
1054 0         0 $self->{reconnect_timer}->stop;
1055              
1056             my $f = $self->connect(
1057             host => $host,
1058             nick => $opts->{nick},
1059             port => $opts->{port} || $s->{port},
1060             SSL => $opts->{SSL} || $s->{SSL},
1061             ident => $opts->{ident} || $s->{ident},
1062             pass => $opts->{pass} || $s->{pass},
1063             local_host => $opts->{local_host},
1064 0     0   0 on_error => sub { warn "Empty closure" },
1065 0   0     0 );
      0        
      0        
      0        
1066              
1067 0     0   0 $f->on_done( sub { $cinv->respond( "Connected to $host", level => 1 ) } );
  0         0  
1068 0     0   0 $f->on_fail( sub { $cinv->responderr( "Unable to connect to $host - $_[0]", level => 3 ) } );
  0         0  
1069              
1070 0         0 return ( "Connecting to $host ..." );
1071 4     4   1840 }
  4         6  
  4         17  
1072              
1073             sub command_reconnect
1074             : Command_description("Disconnect then reconnect to the IRC server")
1075             : Command_arg('message', eatall => 1)
1076             {
1077 0     0 0 0 my $self = shift;
1078 0         0 my ( $message ) = @_;
1079              
1080 0         0 my $irc = $self->{irc};
1081              
1082 0         0 $irc->send_message( "QUIT", undef, $message );
1083              
1084 0         0 $irc->close;
1085              
1086 0         0 $self->{no_reconnect_on_close} = 1;
1087              
1088             $self->reconnect
1089 0     0   0 ->on_done( sub { undef $self->{no_reconnect_on_close} });
  0         0  
1090              
1091 0         0 return;
1092 4     4   1002 }
  4         5  
  4         15  
1093              
1094             sub command_disconnect
1095             : Command_description("Disconnect from the IRC server")
1096             : Command_arg('message?', eatall => 1)
1097             {
1098 0     0 0 0 my $self = shift;
1099 0         0 my ( $message ) = @_;
1100              
1101 0         0 my $irc = $self->{irc};
1102              
1103 0 0       0 if( $irc->read_handle ) {
1104 0 0       0 $irc->send_message( "QUIT", undef, defined $message ? ( $message ) : () );
1105 0         0 $irc->close;
1106              
1107 0         0 $self->{no_reconnect_on_close} = 1;
1108             }
1109             else {
1110 0         0 my $timer = $self->{reconnect_timer};
1111 0 0       0 $timer->stop if $timer->is_running;
1112 0         0 $self->set_network_status( "disconnected" );
1113             }
1114              
1115 0         0 return;
1116 4     4   1035 }
  4         8  
  4         13  
1117              
1118             sub command_join
1119             : Command_description("Join a channel")
1120             : Command_arg('channel')
1121             : Command_opt('key=$', desc => "join key")
1122             {
1123 0     0 0 0 my $self = shift;
1124 0         0 my ( $channel, $opts, $cinv ) = @_;
1125              
1126 0         0 my $irc = $self->{irc};
1127              
1128 0         0 my $chanobj = $self->get_channel_or_create( $channel );
1129              
1130 0         0 $chanobj->reify;
1131              
1132             $chanobj->join(
1133             key => $opts->{key},
1134             on_joined => sub {
1135 0     0   0 $cinv->respond( "Joined $channel", level => 1 );
1136             },
1137             on_join_error => sub {
1138 0     0   0 $cinv->responderr( "Cannot join $channel - $_[0]", level => 3 );
1139             },
1140 0         0 );
1141              
1142 0         0 return;
1143 4     4   1163 }
  4         8  
  4         16  
1144              
1145             sub command_part
1146             : Command_description("Part a channel")
1147             : Command_arg('channel')
1148             : Command_arg('message?', eatall => 1)
1149             {
1150 0     0 0 0 my $self = shift;
1151 0         0 my ( $channel, $message, $cinv ) = @_;
1152              
1153 0 0       0 my $chanobj = $self->get_channel_if_exists( $channel )
1154             or return "No such channel $channel";
1155              
1156             $chanobj->part(
1157             message => $message,
1158              
1159             on_parted => sub {
1160 0     0   0 $cinv->respond( "Parted $channel", level => 1 );
1161 0         0 $chanobj->destroy;
1162             },
1163             on_part_error => sub {
1164 0     0   0 $cinv->respond( "Cannot part $channel - $_[0]", level => 3 );
1165             },
1166 0         0 );
1167              
1168 0         0 return;
1169 4     4   1095 }
  4         7  
  4         14  
1170              
1171             sub command_query
1172             : Command_description("Open a private message window to a user")
1173             : Command_arg('nick')
1174             {
1175 0     0 0 0 my $self = shift;
1176 0         0 my ( $nick, $cinv ) = @_;
1177              
1178 0         0 my $userobj = $self->get_user_or_create( $nick );
1179              
1180 0         0 $userobj->reify;
1181              
1182             # TODO: Focus it
1183              
1184 0         0 return;
1185 4     4   871 }
  4         6  
  4         13  
1186              
1187             sub command_msg
1188             : Command_description("Send a PRIVMSG to a target")
1189             : Command_arg('target')
1190             : Command_arg('text', eatall => 1)
1191             {
1192 0     0 0 0 my $self = shift;
1193 0         0 my ( $target, $text ) = @_;
1194              
1195 0 0       0 if( my $targetobj = $self->get_target_if_exists( $target ) ) {
1196 0         0 $targetobj->msg( $text );
1197             }
1198             else {
1199 0         0 my $irc = $self->{irc};
1200 0         0 $irc->send_message( "PRIVMSG", undef, $target, $text );
1201             }
1202              
1203 0         0 return;
1204 4     4   890 }
  4         7  
  4         14  
1205              
1206             sub command_notice
1207             : Command_description("Send a NOTICE to a target")
1208             : Command_arg('target')
1209             : Command_arg('text', eatall => 1)
1210             {
1211 0     0 0 0 my $self = shift;
1212 0         0 my ( $target, $text ) = @_;
1213              
1214 0 0       0 if( my $targetobj = $self->get_target_if_exists( $target ) ) {
1215 0         0 $targetobj->notice( $text );
1216             }
1217             else {
1218 0         0 my $irc = $self->{irc};
1219 0         0 $irc->send_message( "NOTICE", undef, $target, $text );
1220             }
1221              
1222 0         0 return;
1223 4     4   948 }
  4         5  
  4         19  
1224              
1225             sub command_quote
1226             : Command_description("Send a raw IRC command")
1227             : Command_arg('cmd')
1228             : Command_arg('args', collect => 1)
1229             {
1230 0     0 0 0 my $self = shift;
1231 0         0 my ( $cmd, $args ) = @_;
1232              
1233 0         0 my $irc = $self->{irc};
1234              
1235 0         0 $irc->send_message( $cmd, undef, @$args );
1236              
1237 0         0 return;
1238 4     4   820 }
  4         8  
  4         15  
1239              
1240             sub command_away
1241             : Command_description("Set AWAY message")
1242             : Command_arg('message', eatall => 1)
1243             {
1244 0     0 0 0 my $self = shift;
1245 0         0 my ( $message ) = @_;
1246              
1247 0         0 my $irc = $self->{irc};
1248              
1249 0 0       0 length $message or $message = "away";
1250              
1251 0         0 $irc->send_message( "AWAY", undef, $message );
1252              
1253 0         0 return;
1254 4     4   827 }
  4         7  
  4         15  
1255              
1256             sub command_unaway
1257             : Command_description("Remove AWAY message")
1258             {
1259 0     0 0 0 my $self = shift;
1260              
1261 0         0 my $irc = $self->{irc};
1262              
1263 0         0 $irc->send_message( "AWAY", undef );
1264              
1265 0         0 return;
1266 4     4   744 }
  4         7  
  4         15  
1267              
1268             sub command_whois
1269             : Command_description("Send a WHOIS query")
1270             : Command_arg('user')
1271             {
1272 0     0 0 0 my $self = shift;
1273 0         0 my ( $user, $cinv ) = @_;
1274              
1275 0         0 my $irc = $self->{irc};
1276 0         0 my $user_folded = $irc->casefold_name( $user );
1277              
1278 0         0 $irc->send_message( "WHOIS", undef, $user );
1279              
1280 0   0     0 my $f = ( $self->{whois_gate_f}{$user_folded} ||= Future->new );
1281             $f->on_done( sub {
1282 0     0   0 my ( $data ) = @_;
1283              
1284 0         0 $cinv->respond( "WHOIS $user:" );
1285 0         0 foreach my $datum ( @$data ) {
1286 0         0 my %d = %$datum;
1287 0         0 my $whois = delete $d{whois};
1288              
1289             $cinv->respond( " $whois - " . join( " ",
1290 0         0 map { my $val = $d{$_};
  0         0  
1291             # 'channels' comes as an ARRAY
1292 0 0       0 ref($val) eq "ARRAY" ? "$_=@{$d{$_}}" : "$_=$d{$_}"
  0         0  
1293             } sort keys %d
1294             ) );
1295             }
1296 0         0 });
1297             $f->on_fail( sub {
1298 0     0   0 my ( $failure ) = @_;
1299 0         0 $cinv->responderr( "Cannot WHOIS $user - $failure" );
1300 0         0 });
1301              
1302 0         0 return ();
1303 4     4   1649 }
  4         6  
  4         58  
1304              
1305             use Circle::Collection
1306             name => 'channels',
1307             storage => 'methods',
1308             attrs => [
1309             name => { desc => "name" },
1310             joined => { desc => "currently JOINed?",
1311             transient => 1,
1312 0 0         show => sub { $_ ? "yes" : "no" },
1313             },
1314             autojoin => { desc => "JOIN automatically when connected",
1315 0 0         show => sub { $_ ? "yes" : "no" },
1316             },
1317 4         47 key => { desc => "join key" },
1318             ],
1319 4     4   822 ;
  4         7  
1320              
1321             sub channels_list
1322             {
1323 0     0 0   my $self = shift;
1324 0           return map { $self->channels_get( $_ ) } sort keys %{ $self->{channels} };
  0            
  0            
1325             }
1326              
1327             sub channels_get
1328             {
1329 0     0 0   my $self = shift;
1330 0           my ( $name ) = @_;
1331              
1332 0 0         my $chan = $self->get_channel_if_exists( $name ) or return undef;
1333              
1334             return {
1335             name => $chan->get_prop_name,
1336             joined => $chan->{state} == Circle::Net::IRC::Channel::STATE_JOINED,
1337 0           ( map { $_ => $chan->{$_} } qw( autojoin key ) ),
  0            
1338             };
1339             }
1340              
1341             sub channels_set
1342             {
1343 0     0 0   my $self = shift;
1344 0           my ( $name, $def ) = @_;
1345              
1346 0 0         my $chanobj = $self->get_channel_if_exists( $name ) or die "Missing channel $name for channels_set";
1347              
1348 0           foreach (qw( autojoin key )) {
1349 0 0         $chanobj->{$_} = $def->{$_} if exists $def->{$_};
1350             }
1351             }
1352              
1353             sub channels_add
1354             {
1355 0     0 0   my $self = shift;
1356 0           my ( $name, $def ) = @_;
1357              
1358 0           my $chanobj = $self->get_channel_or_create( $name );
1359              
1360 0           $chanobj->reify;
1361              
1362 0           foreach (qw( autojoin key )) {
1363 0 0         $chanobj->{$_} = $def->{$_} if exists $def->{$_};
1364             }
1365             }
1366              
1367             sub channels_del
1368             {
1369 0     0 0   my $self = shift;
1370 0           my ( $name, $def ) = @_;
1371              
1372 0 0         my $chanobj = $self->get_channel_if_exists( $name ) or return undef;
1373              
1374 0 0         die "channel is joined" if $chanobj->{state} == Circle::Net::IRC::Channel::STATE_JOINED;
1375              
1376 0           $chanobj->destroy;
1377             }
1378              
1379             sub commandable_parent
1380             {
1381 0     0 0   my $self = shift;
1382 0           return $self->{root};
1383             }
1384              
1385             sub enumerable_name
1386             {
1387 0     0 0   my $self = shift;
1388 0           return $self->get_prop_tag;
1389             }
1390              
1391             sub parent
1392             {
1393 0     0 0   my $self = shift;
1394 0           return $self->{root};
1395             }
1396              
1397             sub enumerate_items
1398             {
1399 0     0 0   my $self = shift;
1400              
1401 0           my %all = ( %{ $self->{channels} }, %{ $self->{users} } );
  0            
  0            
1402              
1403             # Filter only the real ones
1404 0   0       $all{$_}->get_prop_real or delete $all{$_} for keys %all;
1405              
1406 0           return { map { $_->enumerable_name => $_ } values %all };
  0            
1407             }
1408              
1409             sub get_item
1410             {
1411 0     0 0   my $self = shift;
1412 0           my ( $name, $create ) = @_;
1413              
1414 0           foreach my $items ( $self->{channels}, $self->{users} ) {
1415 0 0 0       return $items->{$name} if exists $items->{$name} and $items->{$name}->get_prop_real;
1416             }
1417              
1418 0 0         return $self->get_target_or_create( $name ) if $create;
1419              
1420 0           return undef;
1421             }
1422              
1423             __PACKAGE__->APPLY_Setting( local_host =>
1424             description => "Local bind address",
1425             type => 'str',
1426             );
1427              
1428             __PACKAGE__->APPLY_Setting( nick =>
1429             description => "Initial connection nick",
1430             type => 'str',
1431             storage => 'configured_nick',
1432             );
1433              
1434             __PACKAGE__->APPLY_Setting( use_mirc_colours =>
1435             description => "Use mIRC colouring information",
1436             type => 'bool',
1437             default => 1,
1438             );
1439              
1440             ###
1441             # Widgets
1442             ###
1443              
1444             sub get_widget_statusbar
1445             {
1446 0     0 0   my $self = shift;
1447              
1448 0           my $registry = $self->{registry};
1449              
1450 0           my $statusbar = $registry->construct(
1451             "Circle::Widget::Box",
1452             classes => [qw( status )],
1453             orientation => "horizontal",
1454             );
1455              
1456 0           $statusbar->add( $self->get_widget_netname );
1457              
1458 0           my $nicklabel = $registry->construct(
1459             "Circle::Widget::Label",
1460             classes => [qw( nick )],
1461             );
1462             $self->watch_property( "nick",
1463 0     0     on_updated => sub { $nicklabel->set_prop_text( $_[1] ) }
1464 0           );
1465              
1466 0           $statusbar->add( $nicklabel );
1467              
1468 0           my $awaylabel = $registry->construct(
1469             "Circle::Widget::Label",
1470             classes => [qw( away )],
1471             );
1472             $self->watch_property( "away",
1473 0 0   0     on_updated => sub { $awaylabel->set_prop_text( $_[1] ? "[AWAY]" : "" ) }
1474 0           );
1475              
1476 0           $statusbar->add( $awaylabel );
1477              
1478 0           return $statusbar;
1479             }
1480              
1481             sub get_widget_channel_completegroup
1482             {
1483 0     0 0   my $self = shift;
1484              
1485 0   0       return $self->{widget_channel_completegroup} ||= do {
1486 0           my $registry = $self->{registry};
1487              
1488 0           my $widget = $registry->construct(
1489             "Circle::Widget::Entry::CompleteGroup",
1490             );
1491              
1492             # Have to cache id->name so we can delete properly
1493             # TODO: Consider fixing on_del
1494 0           my %id_to_name;
1495             $self->watch_property( "channels",
1496             on_set => sub {
1497 0     0     my ( undef, $channels ) = @_;
1498 0           $widget->set( map { $id_to_name{$_->id} = $_->name } values %$channels );
  0            
1499             },
1500             on_add => sub {
1501 0     0     my ( undef, $added ) = @_;
1502 0           $widget->add( $id_to_name{$added->id} = $added->name );
1503             },
1504             on_del => sub {
1505 0     0     my ( undef, $deleted_id ) = @_;
1506 0           $widget->remove( delete $id_to_name{$deleted_id} );
1507             },
1508 0           );
1509              
1510 0           $widget->set( keys %{ $self->{channels} } );
  0            
1511              
1512 0           $widget;
1513             };
1514             }
1515              
1516             sub add_entry_widget_completegroups
1517             {
1518 0     0 0   my $self = shift;
1519 0           my ( $entry ) = @_;
1520              
1521 0           $entry->add_prop_completions( $self->get_widget_channel_completegroup );
1522             }
1523              
1524             sub get_widget_commandentry
1525             {
1526 0     0 0   my $self = shift;
1527 0           my $widget = $self->SUPER::get_widget_commandentry;
1528              
1529 0           $self->add_entry_widget_completegroups( $widget );
1530              
1531 0           return $widget;
1532             }
1533              
1534             0x55AA;