File Coverage

blib/lib/POE/Component/IRC/Plugin/MultiProxy/State.pm
Criterion Covered Total %
statement 17 264 6.4
branch 0 108 0.0
condition 0 9 0.0
subroutine 7 27 25.9
pod 6 19 31.5
total 30 427 7.0


line stmt bran cond sub pod time code
1             package POE::Component::IRC::Plugin::MultiProxy::State;
2             BEGIN {
3 1     1   1546 $POE::Component::IRC::Plugin::MultiProxy::State::AUTHORITY = 'cpan:HINRIK';
4             }
5             BEGIN {
6 1     1   16 $POE::Component::IRC::Plugin::MultiProxy::State::VERSION = '0.01';
7             }
8              
9 1     1   9 use strict;
  1         3  
  1         52  
10 1     1   5 use warnings FATAL => 'all';
  1         1  
  1         39  
11 1     1   5 use POE::Filter::IRCD;
  1         3  
  1         29  
12 1     1   9 use POE::Component::IRC::Common qw(parse_user u_irc);
  1         2  
  1         64  
13 1     1   5 use POE::Component::IRC::Plugin qw(:ALL);
  1         2  
  1         3985  
14              
15             sub new {
16 0     0 1   my ($package) = @_;
17 0           return bless { }, $package;
18             }
19              
20             sub PCI_register {
21 0     0 0   my ($self, $irc) = @_;
22 0           $self->{irc} = $irc;
23 0           $self->{filter} = POE::Filter::IRCD->new();
24 0           $irc->plugin_register($self, SERVER => qw(001 away_sync_start away_sync_end join chan_mode chan_sync chan_sync_invex chan_sync_excepts nick_sync raw));
25 0           return 1;
26             }
27              
28             sub PCI_unregister {
29 0     0 0   my ($self, $irc) = @_;
30 0           return 1;
31             }
32              
33             sub S_001 {
34 0     0 0   my ($self, $irc) = splice @_, 0, 2;
35 0           $self->{syncing_away} = { };
36 0           $self->{syncing_op} = { };
37 0           $self->{syncing_join} = { };
38 0           $self->{op_queue} = { };
39 0           $self->{join_queue} = { };
40              
41 0           return PCI_EAT_NONE;
42             }
43              
44             sub S_join {
45 0     0 0   my ($self, $irc) = splice @_, 0, 2;
46 0           my $mapping = $irc->isupport('CASEMAPPING');
47 0           my $unick = u_irc((parse_user(${ $_[0] }))[0], $mapping);
  0            
48 0           my $uchan = u_irc(${ $_[1] }, $mapping);
  0            
49              
50 0 0         if ($unick eq u_irc($irc->nick_name(), $mapping)) {
51 0           $self->{syncing_join}{$uchan} = 1;
52             }
53             else {
54 0           $self->{syncing_join}{$unick}++;
55             }
56              
57 0           return PCI_EAT_NONE;
58             }
59              
60             sub S_away_sync_start {
61 0     0 0   my ($self, $irc) = splice @_, 0, 2;
62 0           my $mapping = $irc->isupport('CASEMAPPING');
63 0           my $uchan = u_irc(${ $_[0] }, $mapping);
  0            
64              
65 0           $self->{syncing_away}{$uchan} = 1;
66 0           return PCI_EAT_NONE;
67             }
68              
69             sub S_away_sync_end {
70 0     0 0   my ($self, $irc) = splice @_, 0, 2;
71 0           my $mapping = $irc->isupport('CASEMAPPING');
72 0           my $uchan = u_irc(${ $_[0] }, $mapping);
  0            
73              
74 0           delete $self->{syncing_away}{$uchan};
75 0           return PCI_EAT_NONE;
76             }
77              
78             sub S_chan_mode {
79 0     0 0   my ($self, $irc) = splice @_, 0, 2;
80 0           my $mapping = $irc->isupport('CASEMAPPING');
81 0           my $uchan = u_irc(${ $_[1] }, $mapping);
  0            
82 0           my $mode = ${ $_[2] };
  0            
83 0           my $unick = u_irc($irc->nick_name(), $mapping);
84              
85 0 0         if ($mode =~ /\+o/) {
86 0           my @operands = split / /, ${ $_[3] };
  0            
87 0 0         if (grep { u_irc($_, $mapping) eq $unick } @operands) {
  0            
88 0           $self->{syncing_op}{$uchan}{invex} = 1;
89 0           $self->{syncing_op}{$uchan}{excepts} = 1;
90             }
91             }
92              
93 0           return PCI_EAT_NONE;
94             }
95              
96             sub S_chan_sync {
97 0     0 0   my ($self, $irc) = splice @_, 0, 2;
98 0           my $mapping = $irc->isupport('CASEMAPPING');
99 0           my $uchan = u_irc(${ $_[0] }, $mapping);
  0            
100              
101 0           delete $self->{syncing_join}{$uchan};
102 0           $self->_flush_queue($self->{join_queue}{$uchan});
103 0           return PCI_EAT_NONE;
104             }
105              
106             sub S_chan_sync_invex {
107 0     0 0   my ($self, $irc) = splice @_, 0, 2;
108 0           my $mapping = $irc->isupport('CASEMAPPING');
109 0           my $uchan = u_irc(${ $_[0] }, $mapping);
  0            
110              
111 0           $self->_flush_queue($self->{op_queue}{$uchan}{invex});
112 0           delete $self->{syncing_op}{$uchan}{invex};
113 0 0         delete $self->{syncing_op}{$uchan} if !keys %{ $self->{syncing_op}{$uchan} };
  0            
114 0           return PCI_EAT_NONE;
115             }
116              
117             sub S_chan_sync_excepts {
118 0     0 0   my ($self, $irc) = splice @_, 0, 2;
119 0           my $mapping = $irc->isupport('CASEMAPPING');
120 0           my $uchan = u_irc(${ $_[0] }, $mapping);
  0            
121              
122 0           $self->_flush_queue($self->{op_queue}{$uchan}{excepts});
123 0           delete $self->{syncing_op}{$uchan}{excepts};
124 0 0         delete $self->{syncing_op}{$uchan} if !keys %{ $self->{syncing_op}{$uchan} };
  0            
125 0           return PCI_EAT_NONE;
126             }
127              
128             sub S_nick_sync {
129 0     0 0   my ($self, $irc) = splice @_, 0, 2;
130 0           my $mapping = $irc->isupport('CASEMAPPING');
131 0           my $unick = u_irc(${ $_[0] }, $mapping);
  0            
132              
133 0           $self->{syncing_join}{$unick}--;
134 0 0         delete $self->{syncing_join}{$unick} if $self->{syncing_join}{$unick} == 0;
135 0           $self->_flush_queue($self->{join_queue}{$unick});
136 0           return PCI_EAT_NONE;
137             }
138              
139             sub S_raw {
140 0     0 0   my ($self, $irc) = splice @_, 0, 2;
141 0           my $mapping = $irc->isupport('CASEMAPPING');
142 0           my $raw_line = ${ $_[0] };
  0            
143 0           my $input = $self->{filter}->get( [ $raw_line ] )->[0];
144              
145             # syncing_join me
146 0 0         if ($input->{command} =~ /315|324|329|352|367|368/) {
147 0 0         if ($input->{params}[1] =~ /[^#&+!]/) {
148 0 0         if ($self->{syncing_join}{u_irc($input->{params}[1], $mapping)}) {
149 0           return PCI_EAT_PLUGIN;
150             }
151             }
152             }
153              
154             # syncing_join other
155 0 0         if ($input->{command} eq '352') {
156 0 0         if ($self->{syncing_join}{u_irc($input->{params}[5], $mapping)}) {
157 0           return PCI_EAT_PLUGIN;
158             }
159             }
160              
161             # syncing_away
162 0 0         if ($input->{command} =~ /315|352/) {
163 0 0         if ($input->{params}[1] =~ /[^#&+!]/) {
164 0 0         if ($self->{syncing_away}{u_irc($input->{params}[1], $mapping)}) {
165 0           return PCI_EAT_PLUGIN;
166             }
167             }
168             }
169              
170             # syncing_op invex
171 0 0         if ($input->{command} =~ /346|347/) {
172 0 0         if ($self->{syncing_op}{u_irc($input->{params}[1], $mapping)}{invex}) {
173 0           return PCI_EAT_PLUGIN;
174             }
175             }
176              
177             # syncing_op excepts
178 0 0         if ($input->{command} =~ /348|349/) {
179 0 0         if ($self->{syncing_op}{u_irc($input->{params}[1], $mapping)}{excepts}) {
180 0           return PCI_EAT_PLUGIN;
181             }
182             }
183              
184 0           return PCI_EAT_NONE;
185             }
186              
187             sub _flush_queue {
188 0     0     my ($self, $queue) = @_;
189 0 0         return if !$queue;
190              
191 0           while (my $request = shift @$queue) {
192 0           my ($callback, $reply, $real_what, $args) = @{ $request };
  0            
193 0           $callback->($_) for $self->$reply($real_what, @{ $args });
  0            
194             }
195              
196 0           return;
197             }
198              
199             sub is_syncing {
200 0     0 1   my ($self, $what) = @_;
201 0           my $mapping = $self->{irc}->isupport('CASEMAPPING');
202 0           my $uwhat = u_irc($what, $mapping);
203              
204 0 0         return 1 if $self->{syncing_join}{$uwhat};
205 0           return;
206             }
207              
208             sub enqueue {
209 0     0 1   my ($self, $callback, $reply, $what, @args) = @_;
210 0           my $mapping = $self->{irc}->isupport('CASEMAPPING');
211 0           my $uwhat = u_irc($what, $mapping);
212              
213 0 0         if ($reply eq 'mode_reply') {
    0          
214 0 0 0       if (grep { defined && $_ eq 'e' } @args && $self->{syncing_op}{$uwhat}{excepts}) {
  0 0 0        
  0 0          
    0          
    0          
215 0           push @{ $self->{op_queue}{$uwhat}{excepts} }, [$callback, $reply, $what, \@args];
  0            
216 0           return;
217             }
218             elsif (grep { defined && $_ eq 'I' } @args && $self->{syncing_op}{$uwhat}{invex}) {
219 0           push @{ $self->{op_queue}{$uwhat}{invex} }, [$callback, $reply, $what, \@args];
  0            
220 0           return;
221             }
222             elsif ($self->{syncing_join}{$uwhat}) {
223 0           push @{ $self->{join_queue}{$uwhat} }, [$callback, $reply, $what, \@args];
  0            
224 0           return;
225             }
226             }
227             elsif ($reply =~ /(?:who|names|topic)_reply/) {
228 0 0         if ($self->{syncing_join}{$uwhat}) {
229 0           push @{ $self->{join_queue}{$uwhat} }, [$callback, $reply, $what, \@args];
  0            
230 0           return;
231             }
232             }
233              
234 0           my @lines = $self->$reply($what, @args);
235 0           $callback->(@lines);
236 0           return;
237             }
238              
239             # handles /^TOPIC (\S+)$/ where $1 is a channel that we're on
240             sub topic_reply {
241 0     0 1   my ($self, $chan) = @_;
242 0           my $irc = $self->{irc};
243 0           my $me = $irc->nick_name();
244 0           my $server = $irc->server_name();
245 0           my @results;
246              
247 0 0         if (!keys %{ $irc->channel_topic($chan) }) {
  0            
248 0           push @results, ":$server 331 $me $chan :No topic is set";
249             }
250             else {
251 0           my $topic_info = $irc->channel_topic($chan);
252 0           push @results, ":$server 332 $me $chan :" . $topic_info->{Value};
253 0           push @results, ":$server 333 $me $chan " . join(' ', @{$topic_info}{qw(SetBy SetAt)});
  0            
254             }
255              
256 0           return @results;
257              
258             }
259              
260             # handles /^NAMES (\S+)$/ where $1 is a channel that we're on
261             sub names_reply {
262 0     0 1   my ($self, $chan) = @_;
263 0           my $irc = $self->{irc};
264 0           my $me = $irc->nick_name();
265 0           my $server = $irc->server_name();
266 0           my $chan_type = '=';
267 0 0         $chan_type = '@' if $irc->is_channel_mode_set($chan, 's');
268 0 0         $chan_type = '*' if $irc->is_channel_mode_set($chan, 'p');
269              
270 0           my @nicks = sort map {
271 0           my $nick = $_;
272 0           my $prefix = '';
273 0 0         $prefix = '+' if $irc->has_channel_voice($chan, $nick);
274 0 0         $prefix = '%' if $irc->is_channel_halfop($chan, $nick);
275 0 0         $prefix = '@' if $irc->is_channel_operator($chan, $nick);
276 0           $prefix . $nick;
277             } $irc->channel_list($chan);
278              
279 0           my $length = length($server) + length($chan) + length($me) + 11;
280 0           my @results;
281 0           my $nick_list = shift @nicks;
282              
283 0           for my $nick (@nicks) {
284 0 0         if (length("$nick_list $nick") + $length <= 510) {
285 0           $nick_list .= " $nick";
286             }
287             else {
288 0           push @results, ":$server 353 $me $chan_type $chan :$nick_list";
289 0           $nick_list = $nick;
290             }
291             }
292              
293 0           push @results, ":$server 353 $me $chan_type $chan :$nick_list";
294 0           push @results, ":$server 366 $me $chan :End of NAMES list";
295              
296 0           return @results;
297             }
298              
299             # handles /^WHO (\S+)$/ where $1 is a channel we're on or a nickname, NOT a mask
300             sub who_reply {
301 0     0 1   my ($self, $who) = @_;
302 0           my $irc = $self->{irc};
303 0           my $me = $irc->nick_name();
304 0           my $server = $irc->server_name();
305 0 0         my $prefix = $who =~ /^[#&+!]/ ? $who : '*';
306              
307 0           my @members;
308 0 0         @members = $irc->channel_list($who) if $irc->is_channel_member($who, $me);
309 0 0         @members = ($who) if $irc->_nick_exists($who);
310              
311 0           my @results;
312 0           for my $member (@members) {
313 0           my ($nick, $user, $host) = parse_user($irc->nick_long_form($member));
314              
315 0 0         my $status = $irc->is_away($nick) ? 'G' : 'H';
316 0 0         $status .= '*' if $irc->is_operator($nick);
317 0 0         $status .= '@' if $irc->is_channel_operator($who, $nick);
318 0 0         $status .= '%' if $irc->is_channel_halfop($who, $nick);
319 0 0 0       $status .= '+' if $irc->has_channel_voice($who, $nick) && !$irc->is_channel_operator($who, $nick);
320              
321 0           my $info = $irc->nick_info($nick);
322 0 0         my $real = defined $info->{Real} ? $info->{Real} : '';
323 0 0         my $user_server = defined $info->{Server} ? $info->{Server} : '*';
324 0 0         my $hops = defined $info->{Hops} ? $info->{Hops} : '*';
325 0           push @results, ":$server 352 $me $prefix $user $host $user_server $nick $status :$hops $real";
326             }
327              
328 0           push @results, ":$server 315 $me $who :End of WHO list";
329 0           return @results;
330             }
331              
332             # handles /^MODE #chan( [Ieb])?$/ and /^MODE our_nick$/
333             sub mode_reply {
334 0     0 0   my ($self, $chan, $type) = @_;
335 0           my $irc = $self->{irc};
336 0           my $mapping = $irc->isupport('CASEMAPPING');
337 0           my $me = $irc->nick_name();
338 0           my $server = $irc->server_name();
339 0           my @results;
340              
341 0 0         if (u_irc($chan, $mapping) eq u_irc($me, $mapping)) {
    0          
    0          
    0          
    0          
342 0           return ":$server 221 $me :+" . $irc->umode();
343             }
344             elsif (!defined $type) {
345 0           my $modes = $irc->channel_modes($chan);
346              
347 0           my $mode_string = '';
348 0           while (my ($mode, $arg) = each %{ $modes }) {
  0            
349 0 0         if (!length $arg) {
350 0           $mode_string .= $mode;
351 0           delete $modes->{$mode};
352             }
353             }
354              
355 0           my @args;
356 0           while (my ($mode, $arg) = each %{ $modes }) {
  0            
357 0           $mode_string .= $mode;
358 0           push @args, $arg;
359             }
360              
361 0 0         $mode_string .= ' ' . join ' ', @args if @args;
362 0           push @results, ":$server 324 $me $chan +$mode_string";
363              
364 0 0         if ($irc->channel_creation_time($chan)) {
365 0           my $time = $irc->channel_creation_time($chan);
366 0           push @results, ":$server 329 $me $chan $time";
367             }
368             }
369             elsif ($type eq 'I') {
370 0           while (my ($mask, $info) = each %{ $irc->channel_invex_list($chan) }) {
  0            
371 0           push @results, ":$server 346 $me $chan $mask " . join (' ', @{$info}{qw(SetBy SetAt)});
  0            
372             }
373 0           push @results, ":$server 347 $me $chan :End of Channel Invite List";
374             }
375             elsif ($type eq 'e') {
376 0           while (my ($mask, $info) = each %{ $irc->channel_except_list($chan) }) {
  0            
377 0           push @results, ":$server 348 $me $chan $mask " . join (' ', @{$info}{qw(SetBy SetAt)});
  0            
378             }
379 0           push @results, ":$server 349 $me $chan :End of Channel Exception List";
380             }
381             elsif ($type eq 'b') {
382 0           while (my ($mask, $info) = each %{ $irc->channel_ban_list($chan) }) {
  0            
383 0           push @results, ":$server 367 $me $chan $mask " . join (' ', @{$info}{qw(SetBy SetAt)});
  0            
384             }
385 0           push @results, ":$server 368 $me $chan :End of Channel Ban List";
386             }
387              
388 0           return @results;
389             }
390              
391             1;
392              
393             =encoding utf8
394              
395             =head1 NAME
396              
397             POE::Compoent::IRC::Plugin::MultiProxy::State - Generates IRC server replies based on information provided by POE::Component::IRC::State
398              
399             =head1 SYNOPSIS
400              
401             use POE::Compoent::IRC::Plugin::MultiProxy::State;
402              
403             $irc->plugin_add('State', POE::Compoent::IRC::Plugin::MultiProxy::State->new());
404              
405             =head1 DESCRIPTION
406              
407             POE::Compoent::IRC::Plugin::MultiProxy::::State is a
408             L plugin. Its role is to use
409             L's information as a
410             cache so that fewer trips will be to the IRC server when clients submit
411             queries. The methods here are only useful for a subset of the use cases that
412             their corresponding commands are capable of, mostly because there are certain
413             cases that only the actual IRC server can handle. However, the methods can
414             handle all the automatic queries that modern IRC clients make, so it does the
415             job.
416              
417             Another thing this plugin does is hide from clients all server replies
418             elicited by L's
419             information gathering.
420              
421             This plugin requires the IRC component to be
422             L or a subclass thereof.
423              
424             =head1 CONSTRUCTOR
425              
426             =head2 C
427              
428             Takes no arguments.
429              
430             Returns a plugin object suitable for feeding to
431             L's C method.
432              
433             =head1 METHODS
434              
435             =head2 C
436              
437             One argument:
438              
439             An IRC channel which the IRC component is on
440              
441             Returns IRC protocol line responses to the C command.
442              
443             =head2 C
444              
445             One argument:
446              
447             An IRC channel which the IRC component is on
448              
449             Returns IRC protocol line responses to the C command.
450              
451             =head2 C
452              
453             One argument:
454              
455             An IRC channel which the IRC component is on, or a known nickname
456              
457             Returns IRC protocol line responses to the C command.
458              
459             =head2 C
460              
461             One or two arguments:
462              
463             The IRC component's nickname, or a channel which the component is on
464             and an optional mode type
465              
466             Returns IRC protocol line responses to the C command.
467              
468             =head2 C
469              
470             In case a client asks for information about a channel while it is being
471             synced, it should call this method, and the information will be provided
472             as soon as it has been gathered.
473              
474             Takes three arguments:
475              
476             A code reference which will be called for every line of response generated,
477             the type of reply being asked for (e.g. 'who_reply'), and the arguments
478             to the corresponding method.
479              
480             =head2 C
481              
482             Takes one argument:
483              
484             An IRC channel.
485              
486             Returns 1 if the channel or nick is being synced, 0 otherwise.
487              
488             =head1 AUTHOR
489              
490             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
491              
492             =cut