root/trunk/plagger/lib/Plagger.pm

Revision 1415 (checked in by miyagawa, 14 years ago)

update Template->new signature

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