root/trunk/plagger/lib/Plagger.pm

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

packaging 0.7.8

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