root/trunk/plagger/lib/Plagger.pm

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

this method is not needed anymore

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