root/trunk/plagger/lib/Plagger.pm

Revision 1936 (checked in by daisuke, 13 years ago)

Allow +My::Custom::Plugin style naming convention

  • Property svn:keywords set to Id Revision
Line 
1 package Plagger;
2 use strict;
3 our $VERSION = '0.7.17';
4
5 use 5.8.1;
6 use Carp;
7 use Data::Dumper;
8 use Encode ();
9 use File::Copy;
10 use File::Basename;
11 use File::Find::Rule (); # don't import rule()!
12 use YAML;
13 use Storable;
14 use UNIVERSAL::require;
15
16 use base qw( Class::Accessor::Fast );
17 __PACKAGE__->mk_accessors( qw(conf update subscription plugins_path cache) );
18
19 use Plagger::Cache;
20 use Plagger::CacheProxy;
21 use Plagger::ConfigLoader;
22 use Plagger::Date;
23 use Plagger::Entry;
24 use Plagger::Feed;
25 use Plagger::Subscription;
26 use Plagger::Template;
27 use Plagger::Update;
28 use Plagger::UserAgent; # use to define $XML::Feed::RSS::PREFERRED_PARSER
29
30 my $context;
31 sub context     { $context }
32 sub set_context { $context = $_[1] }
33
34 sub new {
35     my($class, %opt) = @_;
36
37     my $self = bless {
38         conf  => {},
39         update => Plagger::Update->new,
40         subscription => Plagger::Subscription->new,
41         plugins_path => {},
42         plugins => [],
43         rewrite_tasks => []
44     }, $class;
45
46     my $loader = Plagger::ConfigLoader->new;
47     my $config = $loader->load($opt{config}, $self);
48
49     $loader->load_include($config);
50     $self->{conf} = $config->{global};
51     $self->{conf}->{log} ||= { level => 'debug' };
52
53     if (eval { require Term::Encoding }) {
54         $self->{conf}->{log}->{encoding} ||= Term::Encoding::get_encoding();
55     }
56
57     Plagger->set_context($self);
58
59     $loader->load_recipes($config);
60     $self->load_cache($opt{config});
61     $self->load_plugins(@{ $config->{plugins} || [] });
62     $self->rewrite_config if @{ $self->{rewrite_tasks} };
63
64     $self;
65 }
66
67 sub bootstrap {
68     my $class = shift;
69     my $self = $class->new(@_);
70     $self->run();
71     $self;
72 }
73
74 sub clear_session {
75     my $self = shift;
76     $self->{update}       = Plagger::Update->new;
77     $self->{subscription} = Plagger::Subscription->new;
78 }
79
80 sub add_rewrite_task {
81     my($self, @stuff) = @_;
82     push @{ $self->{rewrite_tasks} }, \@stuff;
83 }
84
85 sub rewrite_config {
86     my $self = shift;
87
88     unless ($self->{config_path}) {
89         $self->log(warn => "config is not loaded from file. Ignoring rewrite tasks.");
90         return;
91     }
92
93     open my $fh, '<', $self->{config_path} or $self->error("$self->{config_path}: $!");
94     my $data = join '', <$fh>;
95     close $fh;
96
97     my $old = $data;
98     my $count;
99
100     # xxx this is a quick hack: It should be a YAML roundtrip maybe
101     for my $task (@{ $self->{rewrite_tasks} }) {
102         my($key, $old_value, $new_value ) = @$task;
103         if ($data =~ s/^(\s+$key:\s+)\Q$old_value\E[ \t]*$/$1$new_value/m) {
104             $count++;
105         } else {
106             $self->log(error => "$key: $old_value not found in $self->{config_path}");
107         }
108     }
109
110     if ($count) {
111         File::Copy::copy( $self->{config_path}, $self->{config_path} . ".bak" );
112         open my $fh, ">", $self->{config_path} or return $self->log(error => "$self->{config_path}: $!");
113         print $fh $data;
114         close $fh;
115
116         $self->log(info => "Rewrote $count password(s) and saved to $self->{config_path}");
117     }
118 }
119
120 sub load_cache {
121     my($self, $config) = @_;
122
123     # use config filename as a base directory for cache
124     my $base = ( basename($config) =~ /^(.*?)\.yaml$/ )[0] || 'config';
125     my $dir  = $base eq 'config' ? ".plagger" : ".plagger-$base";
126
127     # cache is auto-vivified but that's okay
128     $self->{conf}->{cache}->{base} ||= File::Spec->catfile($self->home_dir, $dir);
129
130     $self->cache( Plagger::Cache->new($self->{conf}->{cache}) );
131 }
132
133 sub home_dir {
134     eval { require File::HomeDir };
135     return $@ ? $ENV{HOME} : File::HomeDir->my_home;
136 }
137
138 sub load_plugins {
139     my($self, @plugins) = @_;
140
141     my $plugin_path = $self->conf->{plugin_path} || [];
142        $plugin_path = [ $plugin_path ] unless ref $plugin_path;
143
144     for my $path (@$plugin_path) {
145         opendir my $dir, $path or do {
146             $self->log(warn => "$path: $!");
147             next;
148         };
149         while (my $ent = readdir $dir) {
150             next if $ent =~ /^\./;
151             $ent = File::Spec->catfile($path, $ent);
152             if (-f $ent && $ent =~ /\.pm$/) {
153                 $self->add_plugin_path($ent);
154             } elsif (-d $ent) {
155                 my $lib = File::Spec->catfile($ent, "lib");
156                 if (-e $lib && -d _) {
157                     $self->log(debug => "Add $lib to INC path");
158                     unshift @INC, $lib;
159                 } else {
160                     my $rule = File::Find::Rule->new;
161                     $rule->file;
162                     $rule->name('*.pm');
163                     my @modules = $rule->in($ent);
164                     for my $module (@modules) {
165                         $self->add_plugin_path($module);
166                     }
167                 }
168             }
169         }
170     }
171
172     for my $plugin (@plugins) {
173         $self->load_plugin($plugin) unless $plugin->{disable};
174     }
175 }
176
177 sub add_plugin_path {
178     my($self, $file) = @_;
179
180     my $pkg = $self->extract_package($file)
181         or die "Can't find package from $file";
182     $self->plugins_path->{$pkg} = $file;
183     $self->log(debug => "$file is added as a path to plugin $pkg");
184 }
185
186 sub extract_package {
187     my($self, $file) = @_;
188
189     open my $fh, '<', $file or die "$file: $!";
190     while (<$fh>) {
191         /^package (Plagger::Plugin::.*?);/ and return $1;
192     }
193
194     return;
195 }
196
197 sub autoload_plugin {
198     my($self, $plugin) = @_;
199     unless ($self->is_loaded($plugin->{module})) {
200         $self->load_plugin($plugin);
201     }
202 }
203
204 sub is_loaded {
205     my($self, $stuff) = @_;
206
207     my $sub = ref $stuff && ref $stuff eq 'Regexp'
208         ? sub { $_[0] =~ $stuff }
209         : sub { $_[0] eq $stuff };
210
211     for my $plugin (@{ $self->{plugins} }) {
212         my $module = ref $plugin;
213            $module =~ s/^Plagger::Plugin:://;
214         return 1 if $sub->($module);
215     }
216
217     return;
218 }
219
220 sub load_plugin {
221     my($self, $config) = @_;
222
223     my $module = delete $config->{module};
224     if ($module !~ s/^\+//) {
225         $module =~ s/^Plagger::Plugin:://;
226         $module = "Plagger::Plugin::$module";
227     }
228
229     if ($module->isa('Plagger::Plugin')) {
230         $self->log(debug => "$module is loaded elsewhere ... maybe .t script?");
231     } elsif (my $path = $self->plugins_path->{$module}) {
232         eval { require $path } or die $@;
233     } else {
234         $module->require or die $@;
235     }
236
237     $self->log(info => "plugin $module loaded.");
238
239     my $plugin = $module->new($config);
240     $plugin->cache( Plagger::CacheProxy->new($plugin, $self->cache) );
241     $plugin->register($self);
242
243     push @{$self->{plugins}}, $plugin;
244 }
245
246 sub register_hook {
247     my($self, $plugin, @hooks) = @_;
248     while (my($hook, $callback) = splice @hooks, 0, 2) {
249         # set default rule_hook $hook to $plugin
250         $plugin->rule_hook($hook) unless $plugin->rule_hook;
251
252         push @{ $self->{hooks}->{$hook} }, +{
253             callback  => $callback,
254             plugin    => $plugin,
255         };
256     }
257 }
258
259 sub run_hook {
260     my($self, $hook, $args, $once, $callback) = @_;
261
262     my @ret;
263     for my $action (@{ $self->{hooks}->{$hook} }) {
264         my $plugin = $action->{plugin};
265         if ( $plugin->rule->dispatch($plugin, $hook, $args) ) {
266             my $ret = $action->{callback}->($plugin, $self, $args);
267             $callback->($ret) if $callback;
268             if ($once) {
269                 return $ret if defined $ret;
270             } else {
271                 push @ret, $ret;
272             }
273         } else {
274             push @ret, undef;
275         }
276     }
277
278     return if $once;
279     return @ret;
280 }
281
282 sub run_hook_once {
283     my($self, $hook, $args, $callback) = @_;
284     $self->run_hook($hook, $args, 1, $callback);
285 }
286
287 sub run {
288     my $self = shift;
289
290     $self->autoload_plugin({ module => 'Bundle::Defaults' });
291
292     $self->run_hook('plugin.init');
293     $self->run_hook('subscription.load');
294
295     for my $feed ($self->subscription->feeds) {
296         if (my $sub = $feed->aggregator) {
297             $sub->($self, { feed => $feed });
298             $feed->aggregator(undef); # for cloning
299         } else {
300             my $ok = $self->run_hook_once('customfeed.handle', { feed => $feed });
301             if (!$ok) {
302                 $self->log(error => $feed->url . " is not aggregated by any aggregator");
303                 $self->subscription->delete_feed($feed);
304             }
305         }
306     }
307
308     $self->run_hook('aggregator.finalize');
309     $self->do_run_with_feeds;
310     $self->run_hook('plugin.finalize');
311
312     Plagger->set_context(undef);
313     $self;
314 }
315
316 sub run_with_feeds {
317     my $self = shift;
318     $self->run_hook('plugin.init');
319     $self->do_run_with_feeds;
320     $self->run_hook('plugin.finalize');
321
322     Plagger->set_context(undef);
323     $self;
324 }
325
326 sub do_run_with_feeds {
327     my $self = shift;
328
329     for my $feed ($self->update->feeds) {
330         for my $entry ($feed->entries) {
331             $self->run_hook('update.entry.fixup', { feed => $feed, entry => $entry });
332         }
333         $self->run_hook('update.feed.fixup', { feed => $feed });
334     }
335
336     $self->run_hook('update.fixup');
337
338     $self->run_hook('smartfeed.init');
339     for my $feed ($self->update->feeds) {
340         for my $entry ($feed->entries) {
341             $self->run_hook('smartfeed.entry', { feed => $feed, entry => $entry });
342         }
343         $self->run_hook('smartfeed.feed', { feed => $feed });
344     }
345     $self->run_hook('smartfeed.finalize');
346
347     $self->run_hook('publish.init');
348     for my $feed ($self->update->feeds) {
349         for my $entry ($feed->entries) {
350             $self->run_hook('publish.entry.fixup', { feed => $feed, entry => $entry });
351         }
352
353         $self->run_hook('publish.feed', { feed => $feed });
354
355         for my $entry ($feed->entries) {
356             $self->run_hook('publish.entry', { feed => $feed, entry => $entry });
357         }
358     }
359
360     $self->run_hook('publish.finalize');
361 }
362
363 sub search {
364     my($self, $query) = @_;
365
366     Plagger->set_context($self);
367     $self->run_hook('plugin.init');
368
369     my @feeds;
370     $context->run_hook('searcher.search', { query => $query }, 0, sub { push @feeds, $_[0] });
371
372     Plagger->set_context(undef);
373     return @feeds;
374 }
375
376 sub log {
377     my($self, $level, $msg, %opt) = @_;
378
379     return unless $self->should_log($level);
380
381     # hack to get the original caller as Plugin or Rule
382     my $caller = $opt{caller};
383     unless ($caller) {
384         my $i = 0;
385         while (my $c = caller($i++)) {
386             last if $c !~ /Plugin|Rule/;
387             $caller = $c;
388         }
389         $caller ||= caller(0);
390     }
391
392     chomp($msg);
393     if ($self->conf->{log}->{encoding}) {
394         $msg = Encode::decode_utf8($msg) unless utf8::is_utf8($msg);
395         $msg = Encode::encode($self->conf->{log}->{encoding}, $msg);
396     }
397     warn "$caller [$level] $msg\n";
398 }
399
400 my %levels = (
401     debug => 0,
402     warn  => 1,
403     info  => 2,
404     error => 3,
405 );
406
407 sub should_log {
408     my($self, $level) = @_;
409     $levels{$level} >= $levels{$self->conf->{log}->{level}};
410 }
411
412 sub error {
413     my($self, $msg) = @_;
414     my($caller, $filename, $line) = caller(0);
415     chomp($msg);
416     die "$caller [fatal] $msg at line $line\n";
417 }
418
419 sub dumper {
420     my($self, $stuff) = @_;
421     local $Data::Dumper::Indent = 1;
422     $self->log(debug => Dumper($stuff));
423 }
424
425 sub template {
426     my $self = shift;
427     $self->log(error => "\$context->template is DEPRECATED NOW. use \$plugin->templatize()");
428     my $plugin = shift || (caller)[0];
429     Plagger::Template->new($self, $plugin);
430 }
431
432 sub templatize {
433     my($self, $plugin, $file, $vars) = @_;
434     $self->log(error => "\$context->templatize is DEPRECATED NOW. use \$plugin->templatize()");
435     $plugin->templatize($file, $vars);
436 }
437
438 1;
439 __END__
440
441 =head1 NAME
442
443 Plagger - Pluggable RSS/Atom Aggregator
444
445 =head1 SYNOPSIS
446
447   % plagger -c config.yaml
448
449 =head1 DESCRIPTION
450
451 Plagger is a pluggable RSS/Atom feed aggregator and remixer platform.
452
453 Everything is implemented as a small plugin just like qpsmtpd, blosxom
454 and perlbal. All you have to do is write a flow of aggregation,
455 filters, syndication, publishing and notification plugins in config
456 YAML file.
457
458 See L<http://plagger.org/> for cookbook examples, quickstart document,
459 development community (Mailing List and IRC), subversion repository
460 and bug tracking.
461
462 =head1 BUGS / DEVELOPMENT
463
464 If you find any bug, or you have an idea of nice plugin and want help
465 on it, drop us a line to our mailing list
466 L<http://groups.google.com/group/plagger-dev> or stop by the IRC
467 channel C<#plagger> at irc.freenode.net.
468
469 =head1 AUTHOR
470
471 Tatsuhiko Miyagawa E<lt>miyagawa@bulknews.netE<gt>
472
473 See I<AUTHORS> file for the name of all the contributors.
474
475 =head1 LICENSE
476
477 Except where otherwise noted, Plagger is free software; you can
478 redistribute it and/or modify it under the same terms as Perl itself.
479
480 =head1 SEE ALSO
481
482 L<http://plagger.org/>
483
484 =cut
Note: See TracBrowser for help on using the browser.