File Coverage

blib/lib/Socialtext/Resting/RSS.pm
Criterion Covered Total %
statement 13 15 86.6
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 18 20 90.0


line stmt bran cond sub pod time code
1             package Socialtext::Resting::RSS;
2 1     1   34235 use strict;
  1         3  
  1         33  
3 1     1   5 use warnings;
  1         1  
  1         27  
4 1     1   5 use Carp qw/croak/;
  1         7  
  1         72  
5 1     1   1064 use JSON;
  1         22993  
  1         9  
6 1     1   671 use XML::RSS;
  0            
  0            
7             use Text::Diff;
8             use File::Path qw/mkpath/;
9              
10             =head1 NAME
11              
12             Socialtext::Resting::RSS - Create rss feeds for a Socialtext workspace
13              
14             =cut
15              
16             our $VERSION = '0.01';
17              
18             =head1 SYNOPSIS
19              
20             use Socialtext::Resting::RSS;
21             my $rss = Socialtext::Resting::RSS->new(
22             rester => $rester, output => 'foo.rss',
23             );
24             my $num_changes = $rss->generate;
25              
26             =head1 DESCRIPTION
27              
28             C uses the Socialtext REST API to create a
29             RSS feed that features diffs from the previous version.
30              
31             Patches welcome. Or take over this module! :)
32              
33             =cut
34              
35             sub new {
36             my $class = shift;
37             my $self = {
38             cache_dir => "$ENV{HOME}/.st-rss",
39             max_feed_entries => 20,
40             @_,
41             };
42             if ($self->{output_dir}) {
43             $self->{output} = "$self->{output_dir}/"
44             . $self->{rester}->workspace . ".rss";
45             }
46             for my $m (qw/rester output/) {
47             croak("$m is mandatory!") unless $self->{$m};
48             }
49             bless $self, $class;
50             return $self;
51             }
52              
53             sub generate {
54             my $self = shift;
55              
56             $self->_load_page_cache;
57             $self->_load_latest_pages;
58             $self->_calculate_changed_pages;
59              
60             if (@{ $self->{_changes} }) {
61             $self->_write_new_rss;
62             $self->_rewrite_cache;
63             }
64             return @{ $self->{_changes} };
65             }
66              
67             sub _load_page_cache {
68             my $self = shift;
69             my $r = $self->{rester};
70             my $dir = $self->_page_cache_dir;
71             print "Loading page cache from $dir ...\n";
72              
73             my %cache;
74             my @pages = glob("$dir/*");
75             for my $p (@pages) {
76             open(my $fh, $p) or die "Can't open $p: $!";
77             my $json_text;
78             { local $/; $json_text = <$fh> }
79             close $fh;
80             my $json = jsonToObj($json_text);
81             $cache{$json->{page_id}} = $json;
82             print " Loaded $json->{page_id}\n";
83             }
84             $self->{_page_cache} = \%cache;
85             }
86              
87             sub _load_latest_pages {
88             my $self = shift;
89             my $r = $self->{rester};
90             my $cache = $self->{_page_cache};;
91             print "Loading the latest pages ...\n";
92              
93             $r->accept('perl_hash');
94             my $pages = $r->get_taggedpages('Recent Changes');
95             @$pages = splice @$pages, 0, $self->{max_feed_entries};
96              
97             my %latest;
98             for my $p (@$pages) {
99             $latest{$p->{page_id}} = $p;
100             }
101             $self->{_latest_cache} = \%latest;
102             }
103              
104             sub _calculate_changed_pages {
105             my $self = shift;
106             my $r = $self->{rester};
107             my $old = $self->{_page_cache};
108             my $new = $self->{_latest_cache};
109              
110             my @changes;
111             for my $p (keys %$new) {
112             my $obj = $new->{$p};
113             my $desc;
114             if (!exists $old->{$p}) {
115             warn " $p is new!\n";
116             $desc = $obj->{wikitext} = _fetch_wikitext($r, $obj->{page_id});
117             }
118             elsif ($obj->{revision_id} != $old->{$p}{revision_id}) {
119             warn " $p has changed!\n";
120             $obj->{wikitext} = _fetch_wikitext($r, $obj->{page_id});
121             $desc = $self->_diff_content($old->{$p}, $obj);
122             $desc = "Differences between revision $old->{$p}{revision_count} and $obj->{revision_count}:\n$desc";
123             }
124             next unless $desc;
125              
126             $self->_wikitext_to_html($desc);
127             my $workspace_url = join '/', $r->server, $r->workspace;
128             push @changes, {
129             title => "$obj->{name} - Revision $obj->{revision_count}",
130             link => "$workspace_url/?$obj->{page_id}",
131             description => $desc,
132             modified_time => $obj->{modified_time},
133             };
134             }
135              
136             @changes = sort { $b->{modified_time} <=> $a->{modified_time} } @changes;
137             $self->{_changes} = \@changes;
138             }
139              
140             sub _wikitext_to_html {
141             my $self = shift;
142             $_[0] =~ s#\n#
\n#g;
143             $_[0] =~ s#^(\+.+)$#$1#mg;
144             $_[0] =~ s#^(\-.+)$#$1#mg;
145             }
146              
147             sub _diff_content {
148             my $self = shift;
149             my $old = shift;
150             my $new = shift;
151              
152             my $old_wt = $old->{wikitext};
153             my $new_wt = $new->{wikitext};
154             return diff( \$old_wt, \$new_wt, {
155             FILENAME_A => $old->{revision_count},
156             MTIME_A => $old->{modified_time},
157             FILENAME_B => $new->{revision_count},
158             MTIME_B => $new->{modified_time},
159             },
160             );
161             }
162              
163             sub _rewrite_cache {
164             my $self = shift;
165             my $r = $self->{rester};
166             my $dir = $self->_page_cache_dir;
167             my $new = $self->{_latest_cache};;
168             print "Writing page cache to $dir ...\n";
169              
170             my @cached_pages = glob("$dir/*");
171              
172             # Write new and changed pages
173             for my $p (keys %$new) {
174             warn " writing cache - $p\n";
175             my $filename = "$dir/$new->{$p}{page_id}";
176             open(my $fh, ">$filename") or die "Can't write $filename: $!";
177             print $fh objToJson($new->{$p});
178             close $fh or die "Can't write $filename: $!";
179             }
180              
181             # Pages will never be pruned from this cache, as we never know when
182             # pages get deleted
183             }
184              
185             sub _write_new_rss {
186             my $self = shift;
187             my $r = $self->{rester};
188             my $changes = $self->{_changes};
189             my $filename = $self->{output};;
190              
191             my $rss = new XML::RSS (version => '2.0');
192             $rss->channel(
193             title => 'Socialtext Feed - ' . $r->workspace,
194             link => $r->server . '/' . $r->workspace,
195             language => 'en',
196             description => 'Socialtext Diff Feed',
197             lastBuildDate => scalar(localtime),
198             );
199             for my $c (@$changes) {
200             $rss->add_item(%$c);
201             }
202              
203             $rss->save($filename);
204             print "Wrote $filename\n";
205             }
206              
207             sub _page_cache_dir {
208             my $self = shift;
209             my $r = $self->{rester};
210             my $dir = "$self->{cache_dir}/" . $r->workspace;
211             -d $dir or mkpath $dir or die "Can't mkpath: $dir: $!";
212             return $dir;
213             }
214              
215             sub _fetch_wikitext {
216             my $r = shift;
217             my $page = shift;
218             print " Fetching wikitext for $page\n";
219             $r->accept('text/x.socialtext-wiki');
220             return $r->get_page($page);
221             }
222              
223             =head1 KNOWN ISSUES
224              
225             It rewrites the rss feed every time it is run, losing previous entries. It should keep some count of entries in the rss feed at all time.
226              
227             It could also only check pages with a given tag.
228              
229             =head1 AUTHOR
230              
231             Luke Closs, C<< >>
232              
233             =head1 COPYRIGHT & LICENSE
234              
235             Copyright 2007 Luke Closs, all rights reserved.
236              
237             This program is free software; you can redistribute it and/or modify it
238             under the same terms as Perl itself.
239              
240             =cut
241              
242             1;