File Coverage

blib/lib/Circle/Net/IRC.pm
Criterion Covered Total %
statement 111 708 15.6
branch 0 212 0.0
condition 0 70 0.0
subroutine 37 142 26.0
pod 1 77 1.3
total 149 1209 12.3


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