File Coverage

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