source: wiki-toolkit/trunk/lib/Wiki/Toolkit/Feed/RSS.pm @ 329

Last change on this file since 329 was 329, checked in by nick, 14 years ago

Refactor, and add support for a minimal kind of feed (eg as would come from a search). Also include more geo data in the feed

  • Property svn:executable set to *
File size: 15.6 KB
Line 
1package Wiki::Toolkit::Feed::RSS;
2
3use strict;
4
5use vars qw( @ISA $VERSION );
6$VERSION = '0.10';
7
8use POSIX 'strftime';
9use Time::Piece;
10use URI::Escape;
11use Carp qw( croak );
12
13use Wiki::Toolkit::Feed::Listing;
14@ISA = qw( Wiki::Toolkit::Feed::Listing );
15
16sub new
17{
18    my $class = shift;
19    my $self  = {};
20    bless $self, $class;
21
22    my %args = @_;
23    my $wiki = $args{wiki};
24
25    unless ($wiki && UNIVERSAL::isa($wiki, 'Wiki::Toolkit'))
26    {
27        croak 'No Wiki::Toolkit object supplied';
28    }
29 
30    $self->{wiki} = $wiki;
31 
32    # Mandatory arguments.
33    foreach my $arg (qw/site_name site_url make_node_url recent_changes_link/)
34    {
35        croak "No $arg supplied" unless $args{$arg};
36        $self->{$arg} = $args{$arg};
37    }
38 
39    # Optional arguments.
40    foreach my $arg (qw/site_description interwiki_identifier make_diff_url make_history_url
41                        software_name software_version software_homepage/)
42    {
43        $self->{$arg} = $args{$arg} || '';
44    }
45
46    $self->{timestamp_fmt} = $Wiki::Toolkit::Store::Database::timestamp_fmt;
47    $self->{utc_offset} = strftime "%z", localtime;
48    $self->{utc_offset} =~ s/(..)(..)$/$1:$2/;
49
50    $self;
51}
52
53=item <build_feed_start>
54
55Internal method, to build all the stuff that will go at the start of a feed.
56Generally will output namespaces, headers and so on.
57
58=cut
59sub build_feed_start {
60  my ($self,$feed_timestamp) = @_;
61
62  #"http://purl.org/rss/1.0/modules/wiki/"
63  return qq{<?xml version="1.0" encoding="UTF-8"?>
64
65<rdf:RDF
66 xmlns         = "http://purl.org/rss/1.0/"
67 xmlns:dc      = "http://purl.org/dc/elements/1.1/"
68 xmlns:doap    = "http://usefulinc.com/ns/doap#"
69 xmlns:foaf    = "http://xmlns.com/foaf/0.1/"
70 xmlns:rdf     = "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
71 xmlns:rdfs    = "http://www.w3.org/2000/01/rdf-schema#"
72 xmlns:modwiki = "http://www.usemod.com/cgi-bin/mb.pl?ModWiki"
73 xmlns:geo     = "http://www.w3.org/2003/01/geo/wgs84_pos#"
74 xmlns:space   = "http://frot.org/space/0.1/"
75>
76};
77}
78
79=item <build_feed_mid>
80
81Internal method, to build all the stuff (except items) to go inside the channel
82
83=cut
84sub build_feed_mid {
85    my ($self,$feed_timestamp) = @_;
86
87    my $rss .= qq{<dc:publisher>} . $self->{site_url} . qq{</dc:publisher>\n};
88
89if ($self->{software_name})
90{
91  $rss .= qq{<foaf:maker>
92  <doap:Project>
93    <doap:name>} . $self->{software_name} . qq{</doap:name>\n};
94}
95
96if ($self->{software_name} && $self->{software_homepage})
97{
98  $rss .= qq{    <doap:homepage rdf:resource="} . $self->{software_homepage} . qq{" />\n};
99}
100
101if ($self->{software_name} && $self->{software_version})
102{
103  $rss .= qq{    <doap:release>
104      <doap:Version>
105        <doap:revision>} . $self->{software_version} . qq{</doap:revision>
106      </doap:Version>
107    </doap:release>\n};
108}
109
110if ($self->{software_name})
111{
112  $rss .= qq{  </doap:Project>
113</foaf:maker>\n};
114}
115
116$rss .= qq{<title>}   . $self->{site_name}            . qq{</title>
117<link>}               . $self->{recent_changes_link}  . qq{</link>
118<description>}        . $self->{site_description}     . qq{</description>
119<dc:date>}            . $feed_timestamp                . qq{</dc:date>
120<modwiki:interwiki>}     . $self->{interwiki_identifier} . qq{</modwiki:interwiki>};
121
122   return $rss;
123}
124
125=item <build_feed_end>
126
127Internal method, to build all the stuff that will go at the end of a feed
128
129=cut
130sub build_feed_end {
131    my ($self,$feed_timestamp) = @_;
132
133    return "</rdf:RDF>\n";
134}
135
136
137=item <generate_node_list_feed>
138
139Generate and return an RSS feed for a list of nodes
140
141=cut
142sub generate_node_list_feed {
143  my ($self,$feed_timestamp,@nodes) = @_;
144
145  # Start our feed
146  my $rss = $self->build_feed_start($feed_timestamp);
147  $rss .= qq{
148
149<channel rdf:about="">
150
151};
152  $rss .= $self->build_feed_mid($feed_timestamp);
153
154  # Generate the items list, and the individiual item entries
155  my (@urls, @items);
156  foreach my $node (@nodes)
157  {
158    my $node_name = $node->{name};
159
160    my $timestamp = $node->{last_modified};
161   
162    # Make a Time::Piece object.
163    my $time = Time::Piece->strptime($timestamp, $self->{timestamp_fmt});
164
165    my $utc_offset = $self->{utc_offset};
166   
167    $timestamp = $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
168
169    my $author      = $node->{metadata}{username}[0] || $node->{metadata}{host}[0] || '';
170    my $description = $node->{metadata}{comment}[0]  || '';
171
172    $description .= " [$author]" if $author;
173
174    my $version = $node->{version};
175    my $status  = (1 == $version) ? 'new' : 'updated';
176
177    my $major_change = $node->{metadata}{major_change}[0];
178       $major_change = 1 unless defined $major_change;
179    my $importance = $major_change ? 'major' : 'minor';
180
181    my $url = $self->{make_node_url}->($node_name, $version);
182
183    push @urls, qq{    <rdf:li rdf:resource="$url" />\n};
184
185    my $diff_url = '';
186   
187    if ($self->{make_diff_url})
188    {
189            $diff_url = $self->{make_diff_url}->($node_name);
190    }
191
192    my $history_url = '';
193   
194    if ($self->{make_history_url})
195    {
196      $history_url = $self->{make_history_url}->($node_name);
197    }
198
199    my $node_url = $self->{make_node_url}->($node_name);
200
201    my $rdf_url =  $node_url;
202       $rdf_url =~ s/\?/\?id=/;
203       $rdf_url .= ';format=rdf';
204
205    # make XML-clean
206    my $title =  $node_name;
207       $title =~ s/&/&amp;/g;
208       $title =~ s/</&lt;/g;
209       $title =~ s/>/&gt;/g;
210
211    # Pop the categories into dublin core subject elements
212    #  (http://dublincore.org/usage/terms/history/#subject-004)
213    # TODO: Decide if we should include the "all categories listing" url
214    #        as the scheme (URI) attribute?
215    my $category_rss = "";
216    if($node->{metadata}->{category}) {
217        foreach my $cat (@{ $node->{metadata}->{category} }) {
218            $category_rss .= "  <dc:subject>$cat</dc:subject>\n";
219        }
220    }
221
222    # Include geospacial data, if we have it
223    my $geo_rss = "";
224    if($node->{metadata}->{latitude}) {
225        $geo_rss .= "  <geo:lat>".$node->{metadata}->{latitude}."</geo:lat>\n";
226    }
227    if($node->{metadata}->{longitude}) {
228        $geo_rss .= "  <geo:long>".$node->{metadata}->{longitude}."</geo:long>\n";
229    }
230    if($node->{metadata}->{os_x}) {
231        $geo_rss .= "  <space:os_x>".$node->{metadata}->{os_x}."</space:os_x>\n";
232    }
233    if($node->{metadata}->{os_y}) {
234        $geo_rss .= "  <space:os_y>".$node->{metadata}->{os_y}."</space:os_y>\n";
235    }
236    if($node->{metadata}->{distance}) {
237        $geo_rss .= "  <space:distance>".$node->{metadata}->{distance}."</space:distance>\n";
238    }
239
240    push @items, qq{
241<item rdf:about="$url">
242  <title>$title</title>
243  <link>$url</link>
244  <description>$description</description>
245  <dc:date>$timestamp</dc:date>
246  <dc:contributor>$author</dc:contributor>
247  <modwiki:status>$status</modwiki:status>
248  <modwiki:importance>$importance</modwiki:importance>
249  <modwiki:diff>$diff_url</modwiki:diff>
250  <modwiki:version>$version</modwiki:version>
251  <modwiki:history>$history_url</modwiki:history>
252  <rdfs:seeAlso rdf:resource="$rdf_url" />
253$category_rss
254$geo_rss
255</item>
256};
257  }
258 
259  # Output the items list
260  $rss .= qq{
261
262<items>
263  <rdf:Seq>
264} . join('', @urls) . qq{  </rdf:Seq>
265</items>
266
267</channel>
268};
269
270  # Output the individual item entries
271  $rss .= join('', @items) . "\n";
272
273  # Finish up
274  $rss .= $self->build_feed_end($feed_timestamp);
275 
276  return $rss;   
277}
278
279
280=item B<generate_node_name_distance_feed>
281
282Generate a very cut down rss feed, based just on the nodes, their locations
283(if given), and their distance from a reference location (if given).
284
285Typically used on search feeds.
286
287=cut
288sub generate_node_name_distance_feed {
289  my ($self,$feed_timestamp,@nodes) = @_;
290
291  # Start our feed
292  my $rss = $self->build_feed_start($feed_timestamp);
293  $rss .= qq{
294
295<channel rdf:about="">
296
297};
298  $rss .= $self->build_feed_mid($feed_timestamp);
299
300  # Generate the items list, and the individiual item entries
301  my (@urls, @items);
302  foreach my $node (@nodes)
303  {
304    my $node_name = $node->{name};
305
306    my $url = $self->{make_node_url}->($node_name);
307
308    push @urls, qq{    <rdf:li rdf:resource="$url" />\n};
309
310    my $rdf_url =  $url;
311       $rdf_url =~ s/\?/\?id=/;
312       $rdf_url .= ';format=rdf';
313
314    # make XML-clean
315    my $title =  $node_name;
316       $title =~ s/&/&amp;/g;
317       $title =~ s/</&lt;/g;
318       $title =~ s/>/&gt;/g;
319
320    # What location stuff do we have?
321    my $location = undef;
322    my $distance = undef;
323
324    push @items, qq{
325<item rdf:about="$url">
326  <title>$title</title>
327  <link>$url</link>
328  <rdfs:seeAlso rdf:resource="$rdf_url" />
329</item>
330};
331  }
332 
333  # Output the items list
334  $rss .= qq{
335
336<items>
337  <rdf:Seq>
338} . join('', @urls) . qq{  </rdf:Seq>
339</items>
340
341</channel>
342};
343
344  # Output the individual item entries
345  $rss .= join('', @items) . "\n";
346
347  # Finish up
348  $rss .= $self->build_feed_end($feed_timestamp);
349 
350  return $rss;   
351}
352
353=item B<feed_timestamp>
354
355Generate the timestamp for the RSS, based on the newest node (if available)
356
357=cut
358sub feed_timestamp
359{
360    my ($self, $newest_node) = @_;
361
362    if ($newest_node->{last_modified})
363    {
364        my $time = Time::Piece->strptime( $newest_node->{last_modified}, $self->{timestamp_fmt} );
365
366        my $utc_offset = $self->{utc_offset};
367
368        return $time->strftime( "%Y-%m-%dT%H:%M:%S$utc_offset" );
369    }
370    else
371    {
372        return '1970-01-01T00:00:00+0000';
373    }
374}
375
376# Compatibility method - use feed_timestamp with a node instead
377sub rss_timestamp {
378    my ($self, %args) = @_;
379
380    warn("Old style method used - please convert to calling feed_timestamp with a node!");
381    my $feed_timestamp = $self->feed_timestamp(
382                              $self->fetch_newest_for_recently_changed(%args)
383    );
384    return $feed_timestamp;
385}
386
3871;
388
389__END__
390
391=head1 NAME
392
393  Wiki::Toolkit::Feed::RSS - Output RecentChanges RSS for Wiki::Toolkit.
394
395=head1 DESCRIPTION
396
397This is an alternative access to the recent changes of a Wiki::Toolkit
398wiki. It outputs RSS as described by the ModWiki proposal at
399L<http://www.usemod.com/cgi-bin/mb.pl?ModWiki>
400
401=head1 SYNOPSIS
402
403  use Wiki::Toolkit;
404  use Wiki::Toolkit::Feed::RSS;
405
406  my $wiki = CGI::Wiki->new( ... );  # See perldoc Wiki::Toolkit
407
408  # Set up the RSS feeder with the mandatory arguments - see
409  # C<new()> below for more, optional, arguments.
410  my $rss = Wiki::Toolkit::Feed::RSS->new(
411    wiki                => $wiki,
412    site_name           => 'My Wiki',
413    site_url            => 'http://example.com/',
414    make_node_url       => sub
415                           {
416                             my ($node_name, $version) = @_;
417                             return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
418                           },
419    recent_changes_link => 'http://example.com/?RecentChanges',
420  );
421
422  print "Content-type: application/xml\n\n";
423  print $rss->recent_changes;
424
425=head1 METHODS
426
427=head2 C<new()>
428
429  my $rss = Wiki::Toolkit::Feed::RSS->new(
430    # Mandatory arguments:
431    wiki                 => $wiki,
432    site_name            => 'My Wiki',
433    site_url             => 'http://example.com/',
434    make_node_url        => sub
435                            {
436                              my ($node_name, $version) = @_;
437                              return 'http://example.com/?id=' . uri_escape($node_name) . ';version=' . uri_escape($version);
438                            },
439    recent_changes_link  => 'http://example.com/?RecentChanges',
440
441    # Optional arguments:
442    site_description     => 'My wiki about my stuff',
443    interwiki_identifier => 'MyWiki',
444    make_diff_url        => sub
445                            {
446                              my $node_name = shift;
447                              return 'http://example.com/?diff=' . uri_escape($node_name)
448                            },
449    make_history_url     => sub
450                            {
451                              my $node_name = shift;
452                              return 'http://example.com/?hist=' . uri_escape($node_name)
453                            },
454    software_name        => $your_software_name,     # e.g. "CGI::Wiki"
455    software_version     => $your_software_version,  # e.g. "0.73"
456    software_homepage    => $your_software_homepage, # e.g. "http://search.cpan.org/dist/Wiki-Toolkit/"
457  );
458
459C<wiki> must be a L<Wiki::Toolkit> object. C<make_node_url>, and
460C<make_diff_url> and C<make_history_url>, if supplied, must be coderefs.
461
462The mandatory arguments are:
463
464=over 4
465
466=item * wiki
467
468=item * site_name
469
470=item * site_url
471
472=item * make_node_url
473
474=item * recent_changes_link
475
476=back
477
478The three optional arguments
479
480=over 4
481
482=item * software_name
483
484=item * software_version
485
486=item * software_homepage
487
488=back
489
490are used to generate DOAP (Description Of A Project - see L<http://usefulinc.com/doap>) metadata
491for the feed to show what generated it.
492
493=head2 C<recent_changes()>
494
495  $wiki->write_node(
496                     'About This Wiki',
497                     'blah blah blah',
498                                 $checksum,
499                           {
500                       comment  => 'Stub page, please update!',
501                                   username => 'Fred',
502                     }
503  );
504
505  print "Content-type: application/xml\n\n";
506  print $rss->recent_changes;
507
508  # Or get something other than the default of the latest 15 changes.
509  print $rss->recent_changes( items => 50 );
510  print $rss->recent_changes( days => 7 );
511
512  # Or ignore minor edits.
513  print $rss->recent_changes( ignore_minor_edits => 1 );
514
515  # Personalise your feed further - consider only changes
516  # made by Fred to pages about bookshops.
517  print $rss->recent_changes(
518             filter_on_metadata => {
519                         username => 'Fred',
520                         category => 'Bookshops',
521                       },
522              );
523
524If using C<filter_on_metadata>, note that only changes satisfying
525I<all> criteria will be returned.
526
527B<Note:> Many of the fields emitted by the RSS generator are taken
528from the node metadata. The form of this metadata is I<not> mandated
529by L<Wiki::Toolkit>. Your wiki application should make sure to store some or
530all of the following metadata when calling C<write_node>:
531
532=over 4
533
534=item B<comment> - a brief comment summarising the edit that has just been made; will be used in the RDF description for this item.  Defaults to the empty string.
535
536=item B<username> - an identifier for the person who made the edit; will be used as the Dublin Core contributor for this item, and also in the RDF description.  Defaults to the empty string.
537
538=item B<host> - the hostname or IP address of the computer used to make the edit; if no username is supplied then this will be used as the Dublin Core contributor for this item.  Defaults to the empty string.
539
540=item B<major_change> - true if the edit was a major edit and false if it was a minor edit; used for the importance of the item.  Defaults to true (ie if C<major_change> was not defined or was explicitly stored as C<undef>).
541
542=back
543
544=head2 C<feed_timestamp()>
545
546  print $rss->feed_timestamp();
547
548Returns the timestamp of the feed in POSIX::strftime style ("Tue, 29 Feb 2000
54912:34:56 GMT"), which is equivalent to the timestamp of the most recent item
550in the feed. Takes the same arguments as recent_changes(). You will most likely
551need this to print a Last-Modified HTTP header so user-agents can determine
552whether they need to reload the feed or not.
553 
554=head1 SEE ALSO
555
556=over 4
557
558=item * L<Wiki::Toolkit>
559
560=item * L<http://web.resource.org/rss/1.0/spec>
561
562=item * L<http://www.usemod.com/cgi-bin/mb.pl?ModWiki>
563
564=back
565
566=head1 MAINTAINER
567
568The Wiki::Toolkit project. Originally by Kake Pugh <kake@earth.li>.
569
570=head1 COPYRIGHT AND LICENSE
571
572Copyright 2003-4 Kake Pugh.
573Copyright 2005 Earle Martin.
574Copyright 2006 the Wiki::Toolkit team
575
576This module is free software; you can redistribute it and/or modify it
577under the same terms as Perl itself.
578
579=head1 THANKS
580
581The members of the Semantic Web Interest Group channel on irc.freenode.net,
582#swig, were very useful in the development of this module.
583
584=cut
Note: See TracBrowser for help on using the repository browser.