File Coverage

blib/lib/Circle/Net/IRC/Channel.pm
Criterion Covered Total %
statement 74 496 14.9
branch 0 82 0.0
condition 0 25 0.0
subroutine 25 82 30.4
pod 0 44 0.0
total 99 729 13.5


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-2013 -- leonerd@leonerd.org.uk
4              
5             package Circle::Net::IRC::Channel;
6              
7 4     4   15 use strict;
  4         5  
  4         90  
8 4     4   11 use warnings;
  4         4  
  4         71  
9 4     4   77 use 5.010; # //
  4         9  
10 4     4   14 use base qw( Circle::Net::IRC::Target );
  4         4  
  4         1388  
11              
12 4     4   20 use Carp;
  4         5  
  4         210  
13              
14 4     4   19 use Circle::TaggedString;
  4         4  
  4         90  
15              
16 4     4   15 use Circle::Widget::Box;
  4         3  
  4         76  
17 4     4   15 use Circle::Widget::Entry;
  4         4  
  4         101  
18 4     4   1426 use Circle::Widget::Label;
  4         8  
  4         100  
19              
20 4     4   17 use POSIX qw( strftime );
  4         3  
  4         36  
21              
22 4     4   222 use constant STATE_UNJOINED => 0;
  4         6  
  4         192  
23 4     4   17 use constant STATE_JOINING => 1;
  4         5  
  4         158  
24 4     4   15 use constant STATE_JOINED => 2;
  4         6  
  4         153  
25 4     4   13 use constant STATE_PARTING => 3;
  4         5  
  4         10493  
26              
27             sub new
28             {
29 0     0 0   my $class = shift;
30 0           my $self = $class->SUPER::new( @_ );
31              
32 0           $self->{state} = STATE_UNJOINED;
33              
34 0           return $self;
35             }
36              
37             sub init_prop_occupant_summary
38             {
39 0     0 0   return { total => 0 };
40             }
41              
42             sub on_connected
43             {
44 0     0 0   my $self = shift;
45 0           $self->SUPER::on_connected;
46              
47 0 0 0       if( $self->{autojoin} || $self->{rejoin_on_connect} ) {
48             $self->join(
49 0     0     on_joined => sub { undef $self->{rejoin_on_connect} }
50             # TODO: something about errors
51 0           );
52             }
53             }
54              
55             sub on_disconnected
56             {
57 0     0 0   my $self = shift;
58 0           $self->SUPER::on_disconnected( @_ );
59              
60 0 0         $self->{rejoin_on_connect} = 1 if $self->{state} == STATE_JOINED;
61              
62 0           $self->{state} = STATE_UNJOINED;
63             }
64              
65             sub join
66             {
67 0     0 0   my $self = shift;
68 0           my %args = @_;
69              
70 0           my $on_joined = $args{on_joined};
71 0 0         ref $on_joined eq "CODE" or croak "Expected 'on_joined' as CODE ref";
72              
73 0 0         $self->{state} == STATE_UNJOINED or
74             croak "Cannot join except in UNJOINED state";
75              
76 0           $self->{state} = STATE_JOINING;
77              
78 0   0       my $key = $args{key} // $self->{key};
79              
80 0           my $net = $self->{net};
81 0           $net->do_join( $self->get_prop_name, $key );
82              
83 0           $self->{on_joined} = $on_joined;
84 0           $self->{on_join_error} = $args{on_join_error};
85             }
86              
87             sub kick
88             {
89 0     0 0   my $self = shift;
90 0           my ( $nick, $message ) = @_;
91              
92 0           my $irc = $self->{irc};
93 0           $irc->send_message( "KICK", undef, $self->get_prop_name, $nick, $message );
94             }
95              
96             sub mode
97             {
98 0     0 0   my $self = shift;
99 0           my ( $modestr, @args ) = @_;
100              
101 0           my $irc = $self->{irc};
102 0           $irc->send_message( "MODE", undef, $self->get_prop_name, $modestr, @args );
103             }
104              
105             sub method_mode
106             {
107 0     0 0   my $self = shift; my $ctx = shift;
  0            
108 0           my ( $modestr, $argsarray ) = @_;
109 0           $self->mode( $modestr, @$argsarray );
110             }
111              
112             sub part
113             {
114 0     0 0   my $self = shift;
115 0           my %args = @_;
116              
117 0           my $on_parted = $args{on_parted};
118 0 0         ref $on_parted eq "CODE" or croak "Expected 'on_parted' as CODE ref";
119              
120 0 0         $self->{state} == STATE_JOINED or
121             croak "Cannot part except in JOINED state";
122              
123 0           $self->{state} = STATE_PARTING;
124              
125 0           my $irc = $self->{irc};
126 0 0         $irc->send_message( "PART", undef, $self->get_prop_name, defined $args{message} ? $args{message} : ( "" ) );
127              
128 0           $self->{on_parted} = $on_parted;
129             }
130              
131             sub topic
132             {
133 0     0 0   my $self = shift;
134 0           my ( $topic ) = @_;
135              
136 0           my $irc = $self->{irc};
137 0           $irc->send_message( "TOPIC", undef, $self->get_prop_name, $topic );
138             }
139              
140             sub method_topic
141             {
142 0     0 0   my $self = shift; my $ctx = shift;
  0            
143 0           $self->topic( @_ );
144             }
145              
146             sub user_leave
147             {
148 0     0 0   my $self = shift;
149 0           my ( $nick_folded ) = @_;
150              
151 0           $self->del_prop_occupants( $nick_folded );
152 0           $self->post_update_occupants;
153             }
154              
155             sub gen_modestr
156             {
157 0     0 0   my $self = shift;
158              
159             # This is a dynamic property
160              
161 0           my $mode = $self->get_prop_mode;
162              
163             # Order the mode as the server declares
164              
165 0           my $irc = $self->{irc};
166 0           my $channelmodes = $irc->server_info( "channelmodes" );
167              
168 0           my @modes = sort { index( $channelmodes, $a ) <=> index( $channelmodes, $b ) } keys %$mode;
  0            
169              
170 0           my $str = "+";
171 0           my @args;
172              
173 0           foreach my $modechar ( @modes ) {
174 0           $str .= $modechar;
175 0 0         push @args, $mode->{$modechar} if length $mode->{$modechar};
176             }
177              
178 0           return CORE::join( " ", $str, @args );
179             }
180              
181             sub apply_modes
182             {
183 0     0 0   my $self = shift;
184 0           my ( $modes ) = @_;
185              
186 0           my @mode_added;
187             my @mode_deleted;
188              
189 0           my $irc = $self->{irc};
190 0           my $PREFIX_FLAGS = $irc->isupport( "prefix_flags" );
191              
192 0           foreach my $m ( @$modes ) {
193 0           my ( $type, $sense, $mode ) = @{$m}{qw( type sense mode )};
  0            
194              
195 0 0         my $pm = $sense > 0 ? "+" :
    0          
196             $sense < 0 ? "-" :
197             "";
198              
199 0 0         if( !defined $type ) {
    0          
    0          
    0          
    0          
200 0           print STDERR "TODO: Undefined type for chanmode $mode\n";
201             }
202             elsif( $type eq 'list' ) {
203 0           print STDERR "TODO: A list chanmode $pm$mode $m->{value}\n";
204             }
205             elsif( $type eq 'occupant' ) {
206 0           my $flag = $m->{flag};
207 0           my $nick_folded = $m->{nick_folded};
208              
209 0           my $occupant = $self->get_prop_occupants->{$nick_folded};
210              
211 0 0         if( $sense > 0 ) {
212 0           my $flags = $occupant->{flag} . $flag;
213             # Now sort by PREFIX_FLAGS order
214 0           $flags = CORE::join( "", sort { index( $PREFIX_FLAGS, $a ) <=> index( $PREFIX_FLAGS, $b ) } split( m//, $flags ) );
  0            
215 0           $occupant->{flag} = $flags;
216             }
217             else {
218 0           $occupant->{flag} =~ s/\Q$flag//g;
219             }
220              
221             # We're not adding it, we're changing it
222 0           $self->add_prop_occupants( $nick_folded => $occupant );
223 0           $self->post_update_occupants;
224             }
225             elsif( $type eq 'value' ) {
226 0 0         if( $sense > 0 ) {
227 0           push @mode_added, [ $mode, $m->{value} ];
228             }
229             else {
230 0           push @mode_deleted, $mode;
231             }
232             }
233             elsif( $type eq 'bool' ) {
234 0 0         if( $sense > 0 ) {
235 0           push @mode_added, [ $mode, "" ];
236             }
237             else {
238 0           push @mode_deleted, $mode;
239             }
240             }
241             }
242              
243 0 0         if( @mode_added ) {
244             # TODO: Allow CHANGE_ADD messages to add multiple key/value pairs
245 0           foreach my $m ( @mode_added ) {
246 0           $self->add_prop_mode( $m->[0] => $m->[1] );
247             }
248             }
249              
250 0 0         if( @mode_deleted ) {
251 0           $self->del_prop_mode( $_ ) for @mode_deleted;
252             }
253              
254 0 0 0       if( @mode_added or @mode_deleted or !defined $self->get_prop_modestr ) {
      0        
255 0           $self->set_prop_modestr( $self->gen_modestr );
256             }
257             }
258              
259             sub post_update_occupants
260             {
261 0     0 0   my $self = shift;
262              
263 0           my $irc = $self->{irc};
264              
265 0           my %count = map { $_ => 0 } "total", "", split( m//, $irc->isupport( "prefix_flags" ) );
  0            
266              
267 0           my $myflag;
268              
269 0           foreach my $occ ( values %{ $self->get_prop_occupants } ) {
  0            
270 0 0         unless( defined $occ->{nick} ) {
271 0           warn "Have an undefined nick in $occ in $self\n";
272 0           next;
273             }
274 0 0         unless( defined $occ->{flag} ) {
275 0           warn "Have an undefined flag for nick $occ->{nick} in $occ in $self\n";
276 0           next;
277             }
278              
279 0 0         my $flag = $occ->{flag} =~ m/^(.)/ ? $1 : "";
280              
281 0           $count{total}++;
282 0           $count{$flag}++;
283              
284 0 0         $myflag = $flag if $irc->is_nick_me( $occ->{nick} );
285             }
286              
287 0           $self->set_prop_occupant_summary( \%count );
288              
289             # Efficient application of property change
290 0           my $old_myflag = $self->get_prop_my_flag;
291              
292 0 0 0       $self->set_prop_my_flag( $myflag ) if !defined $old_myflag or $old_myflag ne $myflag;
293             }
294              
295             sub on_message_JOIN
296             {
297 0     0 0   my $self = shift;
298 0           my ( $message, $hints ) = @_;
299              
300 0           my $nick = $hints->{prefix_nick};
301              
302 0           my $userhost = "$hints->{prefix_user}\@$hints->{prefix_host}";
303              
304 0 0         if( $hints->{prefix_is_me} ) {
305 0 0         if( $self->{state} != STATE_JOINING ) {
306 0           print STDERR "Received spurious self JOIN notification when wasn't expecting it\n";
307 0           return 0;
308             }
309              
310 0           $self->{state} = STATE_JOINED;
311 0           $self->{on_joined}->( $self );
312              
313 0           $self->fire_event( "self_joined" );
314 0           $self->push_displayevent( "irc.join", { channel => $self->get_prop_name, nick => $nick, userhost => $userhost } );
315 0           $self->bump_level( 1 );
316              
317             # Request the initial mode
318 0           my $irc = $self->{irc};
319 0           $irc->send_message( "MODE", undef, $self->get_prop_name );
320             }
321             else {
322 0           $self->fire_event( "join", $nick );
323 0           $self->push_displayevent( "irc.join", { channel => $self->get_prop_name, nick => $nick, userhost => $userhost } );
324 0           $self->bump_level( 1 );
325              
326 0           my $nick_folded = $hints->{prefix_nick_folded};
327 0           my $newocc = { nick => $nick, flag => "" };
328              
329 0           $self->add_prop_occupants( $nick_folded => $newocc );
330 0           $self->post_update_occupants;
331             }
332              
333 0           return 1;
334             }
335              
336             sub on_message_KICK
337             {
338 0     0 0   my $self = shift;
339 0           my ( $message, $hints ) = @_;
340              
341 0           my $kicker = $hints->{kicker_nick};
342 0           my $kicked = $hints->{kicked_nick};
343 0           my $kickmsg = $hints->{text};
344              
345 0 0         defined $kickmsg or $kickmsg = "";
346              
347 0           my $net = $self->{net};
348 0           my $kickmsg_formatted = $net->format_text( $kickmsg );
349              
350 0           my $irc = $self->{irc};
351 0 0         if( $irc->is_nick_me( $kicked ) ) {
352 0           $self->{state} = STATE_UNJOINED;
353              
354 0           $self->fire_event( "self_parted" );
355 0           $self->push_displayevent( "irc.kick", { channel => $self->get_prop_name, kicker => $kicker, kicked => $kicked, kickmsg => $kickmsg_formatted } );
356 0           $self->bump_level( 1 );
357             }
358             else {
359 0           $self->fire_event( "kick", $kicker, $kicked, $kickmsg );
360 0           $self->push_displayevent( "irc.kick", { channel => $self->get_prop_name, kicker => $kicker, kicked => $kicked, kickmsg => $kickmsg_formatted } );
361 0           $self->bump_level( 1 );
362              
363 0           $self->user_leave( $hints->{kicked_nick_folded} );
364             }
365              
366 0           return 1;
367             }
368              
369             sub on_message_MODE
370             {
371 0     0 0   my $self = shift;
372 0           my ( $message, $hints ) = @_;
373              
374 0           my $modes = $hints->{modes};
375              
376 0           my $nick;
377             my $userhost;
378              
379 0 0         if( defined $hints->{prefix_nick} ) {
380 0           $nick = $hints->{prefix_nick};
381 0           $userhost = "$hints->{prefix_user}\@$hints->{prefix_host}";
382             }
383             else {
384 0           $nick = $userhost = $hints->{prefix_host};
385             }
386              
387 0           $self->apply_modes( $hints->{modes} );
388              
389 0           my $modestr = CORE::join( " ", $hints->{modechars}, @{ $hints->{modeargs} } );
  0            
390              
391             # 'nick' for legacy purposes, 'moder' for new
392 0           $self->push_displayevent( "irc.mode", {
393             channel => $self->get_prop_name,
394             nick => $nick, moder => $nick,
395             userhost => $userhost,
396             mode => $modestr,
397             } );
398 0           $self->bump_level( 1 );
399              
400 0           return 1;
401             }
402              
403             sub on_message_NICK
404             {
405 0     0 0   my $self = shift;
406 0           my ( $message, $hints ) = @_;
407              
408 0           my $oldnick_folded = $hints->{old_nick_folded};
409              
410 0 0         return 0 unless my $occ = $self->get_prop_occupants->{$oldnick_folded};
411              
412 0           my $oldnick = $hints->{old_nick};
413 0           my $newnick = $hints->{new_nick};
414              
415 0           $self->push_displayevent( "irc.nick", { channel => $self->get_prop_name, oldnick => $oldnick, newnick => $newnick } );
416 0           $self->bump_level( 1 );
417              
418 0           my $newnick_folded = $hints->{new_nick_folded};
419              
420 0           $self->del_prop_occupants( $oldnick_folded );
421              
422 0           $occ->{nick} = $newnick;
423 0           $self->add_prop_occupants( $newnick_folded => $occ );
424              
425 0           $self->post_update_occupants;
426              
427 0           return 1;
428             }
429              
430             sub on_message_PART
431             {
432 0     0 0   my $self = shift;
433 0           my ( $message, $hints ) = @_;
434              
435 0           my $nick = $hints->{prefix_nick};
436 0           my $partmsg = $hints->{text};
437              
438 0 0         defined $partmsg or $partmsg = "";
439              
440 0           my $net = $self->{net};
441 0           my $partmsg_formatted = $net->format_text( $partmsg );
442              
443 0           my $userhost = "$hints->{prefix_user}\@$hints->{prefix_host}";
444              
445 0 0         if( $hints->{prefix_is_me} ) {
446 0 0         if( $self->{state} != STATE_PARTING ) {
447 0           print STDERR "Received spurious self PART notification when wasn't expecting it\n";
448 0           return 0;
449             }
450              
451 0           $self->{state} = STATE_UNJOINED;
452              
453 0           $self->fire_event( "self_parted" );
454 0           $self->push_displayevent( "irc.part", { channel => $self->get_prop_name, nick => $nick, userhost => $userhost, partmsg => $partmsg_formatted } );
455 0           $self->bump_level( 1 );
456              
457 0           $self->{on_parted}->( $self );
458             }
459             else {
460 0           $self->fire_event( "part", $nick, $partmsg );
461 0           $self->push_displayevent( "irc.part", { channel => $self->get_prop_name, nick => $nick, userhost => $userhost, partmsg => $partmsg_formatted } );
462 0           $self->bump_level( 1 );
463              
464 0           $self->user_leave( $hints->{prefix_nick_folded} );
465             }
466              
467 0           return 1;
468             }
469              
470             sub on_message_QUIT
471             {
472 0     0 0   my $self = shift;
473 0           my ( $message, $hints ) = @_;
474              
475 0           my $nick_folded = $hints->{prefix_nick_folded};
476              
477 0 0         return 0 unless $self->get_prop_occupants->{$nick_folded};
478              
479 0           my $nick = $hints->{prefix_nick};
480 0           my $quitmsg = $hints->{text};
481              
482 0 0         defined $quitmsg or $quitmsg = "";
483              
484 0           my $net = $self->{net};
485 0           my $quitmsg_formatted = $net->format_text( $quitmsg );
486              
487 0           my $userhost = "$hints->{prefix_user}\@$hints->{prefix_host}";
488              
489 0           $self->push_displayevent( "irc.quit", { channel => $self->get_prop_name, nick => $nick, userhost => $userhost, quitmsg => $quitmsg_formatted } );
490 0           $self->bump_level( 1 );
491              
492 0           $self->user_leave( $nick_folded );
493              
494 0           return 1;
495             }
496              
497             sub on_message_TOPIC
498             {
499 0     0 0   my $self = shift;
500 0           my ( $message, $hints ) = @_;
501              
502 0           my $topic = $hints->{text};
503              
504 0           $self->set_prop_topic( $topic );
505              
506 0           my $nick = $hints->{prefix_name};
507              
508 0           my $userhost = "$hints->{prefix_user}\@$hints->{prefix_host}";
509              
510 0           $self->fire_event( "topic", $nick, $topic );
511 0           $self->push_displayevent( "irc.topic", { channel => $self->get_prop_name, nick => $nick, userhost => $userhost, topic => $topic } );
512 0           $self->bump_level( 1 );
513              
514 0           return 1;
515             }
516              
517             sub on_message_RPL_CHANNELMODEIS
518             {
519 0     0 0   my $self = shift;
520 0           my ( $message, $hints ) = @_;
521              
522 0           $self->apply_modes( $hints->{modes} );
523              
524 0           my $modestr = CORE::join( " ", $hints->{modechars}, @{ $hints->{modeargs} } );
  0            
525              
526 0           $self->push_displayevent( "irc.mode_is", { channel => $self->get_prop_name, mode => $modestr } );
527 0           $self->bump_level( 1 );
528              
529 0           return 1;
530             }
531              
532             sub on_message_RPL_NOTOPIC
533             {
534 0     0 0   my $self = shift;
535 0           my ( $message, $hints ) = @_;
536              
537 0           $self->set_prop_topic( "" );
538              
539 0           return 1;
540             }
541              
542             sub on_message_RPL_TOPIC
543             {
544 0     0 0   my $self = shift;
545 0           my ( $message, $hints ) = @_;
546              
547 0           my $topic = $hints->{text};
548              
549 0           $self->set_prop_topic( $topic );
550              
551 0           $self->fire_event( "topic", undef, $topic );
552 0           $self->push_displayevent( "irc.topic_is", { channel => $self->get_prop_name, topic => $topic } );
553 0           $self->bump_level( 1 );
554              
555 0           return 1;
556             }
557              
558             sub on_message_RPL_TOPICWHOTIME
559             {
560 0     0 0   my $self = shift;
561 0           my ( $message, $hints ) = @_;
562              
563 0           my $timestr = strftime "%Y/%m/%d %H:%M:%S", localtime $hints->{timestamp};
564              
565 0           $self->push_displayevent( "irc.topic_by", { channel => $self->get_prop_name, topic_by => $hints->{topic_nick}, timestamp => $timestr } );
566 0           $self->bump_level( 1 );
567              
568 0           return 1;
569             }
570              
571             sub on_message_RPL_CHANNEL_URL
572             {
573 0     0 0   my $self = shift;
574 0           my ( $message, $hints ) = @_;
575              
576 0           $self->push_displayevent( "irc.text", { server => $hints->{prefix_host}, text => "URL: $hints->{text}" } );
577 0           $self->bump_level( 1 );
578              
579 0           return 1;
580             }
581              
582             sub on_message_RPL_CHANNELCREATED
583             {
584 0     0 0   my $self = shift;
585 0           my ( $message, $hints ) = @_;
586              
587 0           my $timestr = strftime "%Y/%m/%d %H:%M:%S", localtime $hints->{timestamp};
588              
589 0           $self->push_displayevent( "irc.text", { server => $hints->{prefix_host}, text => "Channel created $timestr" } );
590 0           $self->bump_level( 1 );
591              
592 0           return 1;
593             }
594              
595             sub on_message_names
596             {
597 0     0 0   my $self = shift;
598 0           my ( $message, $hints ) = @_;
599              
600 0           $self->set_prop_occupants( $hints->{names} );
601 0           $self->post_update_occupants;
602              
603 0           return 1;
604             }
605              
606             sub command_part
607             : Command_description("Part the channel")
608             : Command_arg('message?', eatall => 1)
609             {
610 0     0 0 0 my $self = shift;
611 0         0 my ( $message, $cinv ) = @_;
612              
613             $self->part(
614             message => $message,
615              
616             on_parted => sub {
617 0     0   0 $cinv->respond( "Parted", level => 1 );
618 0         0 $self->destroy;
619             },
620             on_part_error => sub {
621 0     0   0 $cinv->responderr( "Cannot part - $_[0]", level => 3 );
622             },
623 0         0 );
624              
625 0         0 return;
626 4     4   27 }
  4         11  
  4         27  
627              
628             sub command_mode
629             : Command_description("Change a MODE")
630             : Command_arg('mode')
631             : Command_arg('args', collect => 1)
632             {
633 0     0 0 0 my $self = shift;
634 0         0 my ( $mode, $args ) = @_;
635              
636 0         0 $self->mode( $mode, @$args );
637              
638 0         0 return;
639 4     4   813 }
  4         6  
  4         13  
640              
641             sub command_topic
642             : Command_description("Change the TOPIC")
643             : Command_arg('topic?', eatall => 1)
644             {
645 0     0 0 0 my $self = shift;
646 0         0 my ( $topic ) = @_;
647              
648 0 0       0 if( length $topic ) {
649 0         0 $self->topic( $topic );
650             }
651             else {
652 0         0 $self->push_displayevent( "irc.topic_is", { channel => $self->get_prop_name, topic => $self->get_prop_topic } );
653             }
654              
655 0         0 return;
656 4     4   809 }
  4         5  
  4         13  
657              
658             sub command_names
659             : Command_description("Print a list of users in the channel")
660             : Command_opt('flat=+', desc => "all types of users in one flat list")
661             {
662 0     0 0 0 my $self = shift;
663 0         0 my ( $opts, $cinv ) = @_;
664              
665 0         0 my $occ = $self->get_prop_occupants;
666              
667 0 0       0 if( $opts->{flat} ) {
668 0         0 my @names = map { "$occ->{$_}{flag}$occ->{$_}{nick}" } sort keys %$occ;
  0         0  
669              
670 0         0 $cinv->respond( "Names: " . CORE::join( " ", @names ) );
671 0         0 return;
672             }
673              
674             # Split into groups per flag
675 0         0 my %occgroups;
676 0         0 for my $nick_folded ( keys %$occ ) {
677 0         0 my $flag = substr( $occ->{$nick_folded}{flag}, 0, 1 ); # In case user has several
678 0         0 push @{ $occgroups{ $flag } }, $nick_folded;
  0         0  
679             }
680              
681             # TODO: Ought to obtain this from somewhere - NaIRC maybe?
682 0         0 my %flag_to_desc = (
683             '~' => "Founder",
684             '&' => "Admin",
685             '@' => "Operator",
686             '%' => "Halfop",
687             '+' => "Voice",
688             '' => "User",
689             );
690              
691 0         0 my $irc = $self->{irc};
692 0         0 foreach my $flag ( sort { $irc->cmp_prefix_flags( $b, $a ) } keys %occgroups ) {
  0         0  
693 0         0 my @names = map { "$flag$occ->{$_}{nick}" } sort @{ $occgroups{$flag} };
  0         0  
  0         0  
694              
695 0         0 my $text = Circle::TaggedString->new( $flag_to_desc{$flag} . ": " );
696 0         0 $text->append_tagged( CORE::join( " ", @names ), indent => 1 );
697              
698 0         0 $cinv->respond( $text );
699             }
700              
701 0         0 return;
702 4     4   1617 }
  4         7  
  4         14  
703              
704             sub command_op
705             : Command_description("Give channel operator status to users")
706             : Command_arg('users', collect => 1)
707             {
708 0     0 0 0 my $self = shift;
709 0         0 my ( $users ) = @_;
710              
711 0         0 my @users = @$users;
712 0         0 $self->mode( "+".("o"x@users), @users );
713              
714 0         0 return;
715 4     4   767 }
  4         5  
  4         18  
716              
717             sub command_deop
718             : Command_description("Remove channel operator status from users")
719             : Command_arg('users', collect => 1)
720             {
721 0     0 0 0 my $self = shift;
722 0         0 my ( $users ) = @_;
723              
724 0         0 my @users = @$users;
725 0         0 $self->mode( "-".("o"x@users), @users );
726              
727 0         0 return;
728 4     4   774 }
  4         5  
  4         16  
729              
730             sub command_halfop
731             : Command_description("Give channel half-operator status to users")
732             : Command_arg('users', collect => 1)
733             {
734 0     0 0 0 my $self = shift;
735 0         0 my ( $users ) = @_;
736              
737 0         0 my @users = @$users;
738 0         0 $self->mode( "+".("h"x@users), @users );
739              
740 0         0 return;
741 4     4   726 }
  4         5  
  4         14  
742              
743             sub command_dehalfop
744             : Command_description("Remove channel half-operator status from users")
745             : Command_arg('users', collect => 1)
746             {
747 0     0 0 0 my $self = shift;
748 0         0 my ( $users ) = @_;
749              
750 0         0 my @users = @$users;
751 0         0 $self->mode( "-".("h"x@users), @users );
752              
753 0         0 return;
754 4     4   746 }
  4         7  
  4         15  
755              
756             sub command_voice
757             : Command_description("Give channel voice status to users")
758             : Command_arg('users', collect => 1)
759             {
760 0     0 0 0 my $self = shift;
761 0         0 my ( $users ) = @_;
762              
763 0         0 my @users = @$users;
764 0         0 $self->mode( "+".("v"x@users), @users );
765              
766 0         0 return;
767 4     4   729 }
  4         6  
  4         12  
768              
769             sub command_devoice
770             : Command_description("Remove channel voice status from users")
771             : Command_arg('users', collect => 1)
772             {
773 0     0 0 0 my $self = shift;
774 0         0 my ( $users ) = @_;
775              
776 0         0 my @users = @$users;
777 0         0 $self->mode( "-".("v"x@users), @users );
778              
779 0         0 return;
780 4     4   747 }
  4         7  
  4         13  
781              
782             sub command_kick
783             : Command_description("Kick a user from the channel")
784             : Command_arg('user')
785             : Command_arg('message?', eatall => 1 )
786             {
787 0     0 0   my $self = shift;
788 0           my ( $nick, $message ) = @_;
789              
790 0 0         $message = "" if !defined $message;
791              
792 0           $self->kick( $nick, $message );
793              
794 0           return;
795 4     4   844 }
  4         6  
  4         15  
796              
797             ###
798             # Widget tree
799             ###
800              
801             sub get_widget_statusbar
802             {
803 0     0 0   my $self = shift;
804              
805 0           my $registry = $self->{registry};
806 0           my $net = $self->{net};
807              
808 0           my $statusbar = $registry->construct(
809             "Circle::Widget::Box",
810             classes => [qw( status )],
811             orientation => "horizontal",
812             );
813              
814 0           $statusbar->add( $net->get_widget_netname );
815              
816 0           my $nicklabel = $registry->construct(
817             "Circle::Widget::Label",
818             classes => [qw( nick )],
819             );
820              
821             # TODO: This is hideous...
822 0   0       my $nick = $net->get_prop_nick || $net->{configured_nick};
823 0           my $my_flag = "";
824 0     0     my $updatenicklabel = sub { $nicklabel->set_prop_text( $my_flag . $nick ) };
  0            
825             $net->watch_property( "nick",
826 0     0     on_set => sub { $nick = $_[1]; goto &$updatenicklabel }
  0            
827 0           );
828             $self->watch_property( "my_flag",
829 0     0     on_set => sub { $my_flag = $_[1]; goto &$updatenicklabel }
  0            
830 0           );
831 0           $updatenicklabel->();
832              
833 0           $statusbar->add( $nicklabel );
834              
835 0           my $modestrlabel = $registry->construct(
836             "Circle::Widget::Label",
837             classes => [qw( mode )],
838             );
839             $self->watch_property( "modestr",
840 0   0 0     on_updated => sub { $modestrlabel->set_prop_text( $_[1] || "" ) }
841 0           );
842              
843 0           $statusbar->add( $modestrlabel );
844              
845 0           $statusbar->add_spacer( expand => 1 );
846              
847 0           my $countlabel = $registry->construct(
848             "Circle::Widget::Label",
849             classes => [qw( occupants )],
850             );
851             $self->watch_property( "occupant_summary",
852             on_updated => sub {
853 0     0     my ( $self, $summary ) = @_;
854              
855 0           my $irc = $self->{irc};
856 0   0       my $PREFIX_FLAGS = $irc->isupport( "prefix_flags" ) || "";
857              
858             my $str = "$summary->{total} users [" .
859 0 0         CORE::join( " ", map { "$_$summary->{$_}" } grep { $summary->{$_}||0 > 0 } split( m//, $PREFIX_FLAGS ), "" ) .
  0            
  0            
860             "]";
861              
862 0           $countlabel->set_prop_text( $str );
863             }
864 0           );
865              
866 0           $statusbar->add( $countlabel );
867              
868 0           return $statusbar;
869             }
870              
871             sub get_widget_occupants_completegroup
872             {
873 0     0 0   my $self = shift;
874              
875 0   0       return $self->{widget_occupants_completegroup} ||= do {
876 0           my $registry = $self->{registry};
877              
878 0           my $widget = $registry->construct(
879             "Circle::Widget::Entry::CompleteGroup",
880             suffix_sol => ": ",
881             );
882              
883 0           my %key_to_nick;
884             $self->watch_property( "occupants",
885             on_set => sub {
886 0     0     my ( undef, $occupants ) = @_;
887 0           $widget->set( map { $key_to_nick{$_} = $occupants->{$_}{nick} } keys %$occupants );
  0            
888             },
889             on_add => sub {
890 0     0     my ( undef, $key, $occ ) = @_;
891 0           $widget->add( $key_to_nick{$key} = $occ->{nick} );
892             },
893             on_del => sub {
894 0     0     my ( undef, $key ) = @_;
895 0           $widget->remove( delete $key_to_nick{$key} );
896             },
897 0           );
898              
899 0           my $occupants = $self->get_prop_occupants;
900 0           $widget->set( map { $key_to_nick{$_} = $occupants->{$_}{nick} } keys %$occupants );
  0            
901              
902 0           $widget;
903             };
904             }
905              
906             sub get_widget_commandentry
907             {
908 0     0 0   my $self = shift;
909 0           my $widget = $self->SUPER::get_widget_commandentry;
910              
911 0           $widget->add_prop_completions( $self->get_widget_occupants_completegroup );
912              
913 0           return $widget;
914             }
915              
916             sub make_widget_pre_scroller
917             {
918 0     0 0   my $self = shift;
919 0           my ( $box ) = @_;
920              
921 0           my $registry = $self->{registry};
922              
923             my $topicentry = $registry->construct(
924             "Circle::Widget::Entry",
925             classes => [qw( topic )],
926 0     0     on_enter => sub { $self->topic( $_[0] ) },
927 0           );
928             $self->watch_property( "topic",
929 0     0     on_updated => sub { $topicentry->set_prop_text( $_[1] ) }
930 0           );
931              
932 0           $box->add( $topicentry );
933             }
934              
935             0x55AA;