File Coverage

blib/lib/App/Bondage/Recall.pm
Criterion Covered Total %
statement 11 13 84.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 16 18 88.8


line stmt bran cond sub pod time code
1             package App::Bondage::Recall;
2             BEGIN {
3 1     1   1778 $App::Bondage::Recall::AUTHORITY = 'cpan:HINRIK';
4             }
5              
6 1     1   7 use strict;
  1         2  
  1         41  
7 1     1   6 use warnings FATAL => 'all';
  1         1  
  1         45  
8 1     1   6 use File::Temp qw(tempfile);
  1         2  
  1         62  
9 1     1   721 use POE::Component::IRC::Common qw( parse_user );
  0            
  0            
10             use POE::Component::IRC::Plugin qw( :ALL );
11             use POE::Component::IRC::Plugin::BotTraffic;
12             use POE::Filter::IRCD;
13             use Tie::File;
14              
15             our $VERSION = '1.5';
16              
17             sub new {
18             my ($package, %self) = @_;
19             if (!$self{Mode} || $self{Mode} !~ /missed|all|none/) {
20             $self{Mode} = 'missed';
21             }
22             return bless \%self, $package;
23             }
24              
25             sub PCI_register {
26             my ($self, $irc) = @_;
27            
28             if (!$irc->isa('POE::Component::IRC::State')) {
29             die __PACKAGE__ . " requires PoCo::IRC::State or a subclass thereof\n";
30             }
31            
32             if (!grep { $_->isa('POE::Component::IRC::Plugin::BotTraffic') } values %{ $irc->plugin_list() }) {
33             $irc->plugin_add('BotTraffic', POE::Component::IRC::Plugin::BotTraffic->new());
34             }
35              
36             ($self->{state}) = grep { $_->isa('App::Bondage::State') } values %{ $irc->plugin_list() };
37             $self->{irc} = $irc;
38             $self->{filter} = POE::Filter::IRCD->new();
39             $self->{recall} = [ ];
40             $self->{clients} = 0;
41             $self->{last_detach} = 0;
42            
43             tie @{ $self->{recall} }, 'Tie::File', scalar tempfile() if $self->{Mode} =~ /all|missed/;
44            
45             $irc->raw_events(1);
46             $irc->plugin_register($self, 'SERVER', qw(cap bot_ctcp_action bot_public connected ctcp_action msg public part proxy_authed proxy_close raw));
47             return 1;
48             }
49              
50             sub PCI_unregister {
51             my ($self, $irc) = @_;
52             delete $self->{irc};
53             return 1;
54             }
55              
56             sub S_cap {
57             my ($self, $irc) = splice @_, 0, 2;
58             my $cmd = ${ $_[0] };
59              
60             if ($cmd eq 'ACK') {
61             my $list = ${ $_[1] } eq '*' ? ${ $_[2] } : ${ $_[1] };
62             my @enabled = split / /, $list;
63              
64             if (grep { $_ =~ /^=?identify-msg$/ } @enabled) {
65             $self->{idmsg} = 1;
66             }
67             if (grep { $_ =~ /^-identify-msg$/ } @enabled) {
68             $self->{idmsg} = 0;
69             }
70             }
71             return PCI_EAT_NONE;
72             }
73              
74             sub S_bot_ctcp_action {
75             my ($self, $irc) = splice @_, 0, 2;
76             my $recipients = join (',', @{ ${ $_[0] } });
77             my $msg = ${ $_[1] };
78            
79             if ($self->{Mode} =~ /all|missed/) {
80             my $line = ':' . $irc->nick_long_form($irc->nick_name()) . " PRIVMSG $recipients :\x01ACTION $msg\x01";
81             push @{ $self->{recall} }, $line;
82             }
83            
84             return PCI_EAT_NONE;
85             }
86              
87             sub S_bot_public {
88             my ($self, $irc) = splice @_, 0, 2;
89             my $recipients = join (',', @{ ${ $_[0] } });
90             my $msg = ${ $_[1] };
91              
92             if ($self->{Mode} =~ /all|missed/) {
93             my $line = ':' . $irc->nick_long_form($irc->nick_name()) . " PRIVMSG $recipients :$msg";
94             push @{ $self->{recall} }, $line;
95             }
96            
97             return PCI_EAT_NONE;
98             }
99              
100             sub S_connected {
101             my ($self, $irc) = splice @_, 0, 2;
102            
103             $self->{stash} = [ ];
104             $self->{stashing} = 1;
105             $self->{idmsg} = 0;
106             return PCI_EAT_NONE;
107             }
108              
109             sub S_ctcp_action {
110             my ($self, $irc) = splice @_, 0, 2;
111             my $sender = ${ $_[0] };
112             my $recipients = ${ $_[1] };
113             my $msg = ${ $_[2] };
114              
115             return PCI_EAT_NONE if $self->{clients};
116            
117             for my $recipient (@{ $recipients }) {
118             if ($recipient eq $irc->nick_name()) {
119             my $line = ":$sender PRIVMSG $irc->nick_name :\x01ACTION$msg\x01";
120             push @{ $self->{recall} }, $line;
121             }
122             }
123            
124             return PCI_EAT_NONE;
125             }
126              
127             sub S_msg {
128             my ($self, $irc) = splice @_, 0, 2;
129             my $sender = ${ $_[0] };
130             my $msg = ${ $_[2] };
131            
132             return PCI_EAT_NONE if $self->{clients};
133            
134             my $line = ":$sender PRIVMSG $irc->nick_name :$msg";
135             push @{ $self->{recall} }, $line;
136             return PCI_EAT_NONE;
137             }
138            
139             sub S_part {
140             my ($self, $irc) = splice @_, 0, 2;
141             my $chan = ${ $_[1] };
142              
143             if (my $cycle = grep { $_->isa('POE::Component::IRC::Plugin::CycleEmpty') } values %{ $irc->plugin_list() } ) {
144             return PCI_EAT_NONE if $cycle->cycling($chan);
145             }
146              
147             # too CPU-heavy
148             # if ($self->{Mode} eq 'all') {
149             # # remove all messages related to this channel
150             # my $input = $self->{filter}->get( $self->{recall} );
151             # for my $line (0..$#{ $self->{recall} }) {
152             # if (lc $input->[$line]{params}[0] eq lc $chan) {
153             # delete $self->{recall}[$line];
154             # }
155             # elsif ($input->[$line]{command} =~ /332|333|366/ && lc $input->[$line]{params}[1] eq lc $chan) {
156             # delete $self->{recall}[$line];
157             # }
158             # elsif ($input->[$line]{command} eq '353' && lc $input->[$line]{params}->[2] eq lc $chan) {
159             # delete $self->{recall}[$line];
160             # }
161             # }
162             # }
163              
164             return PCI_EAT_NONE;
165             }
166              
167             sub S_public {
168             my ($self, $irc) = splice @_, 0, 2;
169             my $sender = ${ $_[0] };
170             my $chan = ${ $_[1] }->[0];
171             my $msg = ${ $_[2] };
172              
173             # do this here instead rather than in S_raw so that IDENTIFY-MSG
174             # will by handled by POE::Filter::IRC::Compat
175             if ($self->{Mode} =~ /all|missed/) {
176             push @{ $self->{recall} }, ":$sender PRIVMSG $chan :$msg";
177             }
178              
179             return PCI_EAT_NONE;
180             }
181              
182             sub S_proxy_authed {
183             my ($self, $irc) = splice @_, 0, 2;
184             $self->{clients}++;
185             return PCI_EAT_NONE;
186             }
187              
188             sub S_proxy_close {
189             my ($self, $irc) = splice @_, 0, 2;
190             $self->{clients}--;
191             return if $self->{clients};
192            
193             if ($self->{Mode} eq 'missed') {
194             $self->{recall} = [ ];
195             push @{ $self->{recall} }, $self->_chan_info();
196             }
197             elsif ($self->{Mode} eq 'all') {
198             $self->{last_detach} = $#{ $self->{recall} };
199             }
200            
201             return PCI_EAT_NONE;
202             }
203              
204             sub S_raw {
205             my ($self, $irc) = splice @_, 0, 2;
206             my $raw_line = ${ $_[0] };
207             my $input = $self->{filter}->get( [ $raw_line ] )->[0];
208            
209             if ($self->{stashing}) {
210             # capture all numeric commands until we've got the MOTD
211             if ($input->{command} =~ /\d{3}/) {
212             push @{ $self->{stash} }, $raw_line;
213             }
214             # RPL_ENDOFMOTD / ERR_NOMOTD
215             if ($input->{command} =~ /376|422/) {
216             $self->{stashing} = 0;
217             }
218             }
219            
220             if ($self->{Mode} =~ /all|missed/) {
221             if ($input->{command} eq 'MODE' && $input->{params}[0] =~ /^[#&+!]/) {
222             # channel mode changes
223             push @{ $self->{recall} }, $raw_line;
224             }
225             elsif ($input->{command} =~ /JOIN|KICK|PART|QUIT|NICK|TOPIC/) {
226             # other channel-related things
227             push @{ $self->{recall} }, $raw_line;
228             }
229             elsif ($input->{command} =~ /332|333|353|366/) {
230             # only log these when we've just joined the channel
231             push @{ $self->{recall} }, $raw_line if $self->{state}->is_syncing($input->{params}[0]);
232             }
233             }
234            
235             return PCI_EAT_NONE;
236             }
237              
238             # returns everything that an IRC server would send us upon joining
239             # the channels we're on
240             sub _chan_info {
241             my ($self) = @_;
242             my $irc = $self->{irc};
243             my $state = $self->{state};
244             my $me = $irc->nick_name();
245              
246             my @info;
247             for my $chan (keys %{ $irc->channels() }) {
248             push @info, ':' . $irc->nick_long_form($me) . " JOIN :$chan";
249             push @info, $state->topic_reply($chan) if keys %{ $irc->channel_topic($chan) };
250             push @info, $state->names_reply($chan);
251             }
252              
253             return @info;
254             }
255              
256             sub recall {
257             my ($self) = @_;
258             my $irc = $self->{irc};
259             my $me = $irc->nick_name();
260             my $server = $irc->server_name();
261             my @lines;
262              
263             for my $line (@{ $self->{stash} }) {
264             $line =~ s/^(\S+ +\S+) +\S+ +(.*)/$1 $me $2/;
265             push @lines, $line;
266             }
267            
268             push @lines, ":$server MODE $me :" . $irc->umode() if $irc->umode();
269             push @lines, @{ $self->{recall} };
270             push @lines, ":$server CAP * ACK :identify-msg" if $self->{idmsg};
271              
272             if ($self->{Mode} eq 'all' && $#{ $self->{recall} } > $self->{last_detach}) {
273             # remove all PMs received since we last detached
274             for my $line ($self->{last_detach} .. $#{ $self->{recall} }) {
275             my $in = shift @{ $self->{filter}->get( $self->{recall} ) };
276             if ($in->{command} eq 'PRIVMSG' && $in->{params}[0] !~ /^[#&+!]/) {
277             delete $self->{recall}[$line];
278             }
279             }
280             }
281             elsif ($self->{Mode} eq 'missed') {
282             $self->{recall} = [ ];
283             push @{ $self->{recall} }, $self->_chan_info();
284             }
285             elsif ($self->{Mode} eq 'none') {
286             $self->{recall} = [ ];
287             push @lines, $self->_chan_info();
288             }
289              
290             return @lines;
291             }
292              
293             1;
294              
295             =encoding utf8
296              
297             =head1 NAME
298              
299             App::Bondage::Recall - A PoCo-IRC plugin which can greet proxy clients
300             with the messages they missed while they were away.
301              
302             =head1 SYNOPSIS
303              
304             use App::Bondage::Recall;
305              
306             $irc->plugin_add('Recall', App::Bondage::Recall->new( Mode => 'missed' ));
307              
308             =head1 DESCRIPTION
309              
310             This plugin requires the IRC component to be
311             L or a subclass thereof.
312             It also requires a
313             L
314             to be in the plugin pipeline. It will be added automatically if it is not present.
315              
316             =head1 METHODS
317              
318             =head2 C
319              
320             One optional argument:
321              
322             B<'Mode'>, which public messages you want it to recall. B<'missed'>, the
323             default, makes it only recall public messages that were received while no
324             proxy client was attached. B<'all'> will recall public messages from all
325             channels since they were joined. B<'none'> will recall none. The plugin will
326             always recall missed private messages, regardless of this option.
327              
328             Returns a plugin object suitable for feeding to
329             L's C method.
330              
331             =head1 AUTHOR
332              
333             Hinrik Ern SigurEsson, hinrik.sig@gmail.com
334              
335             =cut