fu: Close-up of Fu, bringing a scoop of water to her mouth (Default)
fu ([personal profile] fu) wrote in [site community profile] changelog2011-10-25 09:13 am

[dw-free] move cgi-bin/lj*.pl files into proper modules (in cgi-bin/LJ)

[commit: http://hg.dwscoalition.org/dw-free/rev/7181c14fbe5a]

http://bugs.dwscoalition.org/show_bug.cgi?id=1726

Move ljfeed.pl to LJ/Feed.pm

Patch by [personal profile] kareila.

Files modified:
  • cgi-bin/LJ/Feed.pm
  • cgi-bin/ljfeed.pl
  • cgi-bin/ljprotocol.pl
  • cgi-bin/modperl_subs.pl
  • t/feed-atom.t
--------------------------------------------------------------------------------
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/LJ/Feed.pm
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/cgi-bin/LJ/Feed.pm	Tue Oct 25 17:14:09 2011 +0800
@@ -0,0 +1,1028 @@
+#!/usr/bin/perl
+# This code was forked from the LiveJournal project owned and operated
+# by Live Journal, Inc. The code has been modified and expanded by
+# Dreamwidth Studios, LLC. These files were originally licensed under
+# the terms of the license supplied by Live Journal, Inc, which can
+# currently be found at:
+#
+# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
+#
+# In accordance with the original license, this code and all its
+# modifications are provided under the GNU General Public License.
+# A copy of that license can be found in the LICENSE file included as
+# part of this distribution.
+
+
+package LJ::Feed;
+use strict;
+
+use LJ::Entry;
+use XML::Atom::Person;
+use XML::Atom::Feed;
+
+my %feedtypes = (
+    rss         => { handler => \&create_view_rss,  need_items => 1 },
+    atom        => { handler => \&create_view_atom, need_items => 1 },
+    foaf        => { handler => \&create_view_foaf,                 },
+    yadis       => { handler => \&create_view_yadis,                },
+    userpics    => { handler => \&create_view_userpics,             },
+    comments    => { handler => \&create_view_comments,             },
+);
+
+sub make_feed
+{
+    my ($r, $u, $remote, $opts) = @_;
+
+    $opts->{pathextra} =~ s!^/(\w+)!!;
+    my $feedtype = $1;
+    my $viewfunc = $feedtypes{$feedtype};
+
+    unless ( $viewfunc && LJ::isu( $u ) ) {
+        $opts->{'handler_return'} = 404;
+        return undef;
+    }
+
+    $r->note('codepath' => "feed.$feedtype") if $r;
+
+    my $dbr = LJ::get_db_reader();
+
+    my $user = $u->user;
+
+    $u->preload_props( qw/ journaltitle journalsubtitle opt_synlevel / );
+
+    LJ::text_out(\$u->{$_})
+        foreach ("name", "url", "urlname");
+
+    # opt_synlevel will default to 'cut'
+    $u->{opt_synlevel} = 'cut'
+        unless $u->{opt_synlevel} &&
+               $u->{opt_synlevel} =~ /^(?:full|cut|summary|title)$/;
+
+    # some data used throughout the channel
+    my $journalinfo = {
+        u         => $u,
+        link      => $u->journal_base . "/",
+        title     => $u->{journaltitle} || $u->name_raw || $u->user,
+        subtitle  => $u->{journalsubtitle} || $u->name_raw,
+        builddate => LJ::time_to_http(time()),
+    };
+
+    # if we do not want items for this view, just call out
+    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
+    return $viewfunc->{handler}->($journalinfo, $u, $opts)
+        unless ($viewfunc->{need_items});
+
+    # for syndicated accounts, redirect to the syndication URL
+    # However, we only want to do this if the data we're returning
+    # is similar. (Not FOAF, for example)
+    if ( $u->is_syndicated ) {
+        my $synurl = $dbr->selectrow_array("SELECT synurl FROM syndicated WHERE userid=$u->{'userid'}");
+        unless ($synurl) {
+            return 'No syndication URL available.';
+        }
+        $opts->{'redir'} = $synurl;
+        return undef;
+    }
+
+    my %FORM = LJ::parse_args( $r->query_string );
+
+    ## load the itemids
+    my (@itemids, @items);
+
+    # for consistency, we call ditemids "itemid" in user-facing settings
+    my $ditemid = defined $FORM{itemid} ? $FORM{itemid} + 0 : 0;
+
+    if ($ditemid) {
+        my $entry = LJ::Entry->new($u, ditemid => $ditemid);
+
+        if (! $entry || ! $entry->valid || ! $entry->visible_to($remote)) {
+            $opts->{'handler_return'} = 404;
+            return undef;
+        }
+
+        @itemids = $entry->jitemid;
+
+        push @items, {
+            itemid => $entry->jitemid,
+            anum => $entry->anum,
+            posterid => $entry->poster->id,
+            security => $entry->security,
+            alldatepart => LJ::alldatepart_s2($entry->eventtime_mysql),
+            rlogtime => $LJ::EndOfTime - LJ::mysqldate_to_time( $entry->logtime_mysql, 0 ),
+        };
+    } else {
+        @items = $u->recent_items(
+            clusterid => $u->{clusterid},
+            clustersource => 'slave',
+            remote => $remote,
+            itemshow => 25,
+            order => 'logtime',
+            tagids => $opts->{tagids},
+            tagmode => $opts->{tagmode},
+            itemids => \@itemids,
+            friendsview => 1,           # this returns rlogtimes
+            dateformat => 'S2',         # S2 format time format is easier
+        );
+    }
+
+    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
+
+    ### load the log properties
+    my %logprops = ();
+    my $logtext;
+    my $logdb = LJ::get_cluster_reader($u);
+    LJ::load_log_props2($logdb, $u->{'userid'}, \@itemids, \%logprops);
+    $logtext = LJ::get_logtext2($u, @itemids);
+
+    # set last-modified header, then let apache figure out
+    # whether we actually need to send the feed.
+    my $lastmod = 0;
+    foreach my $item (@items) {
+        # revtime of the item.
+        my $revtime = $logprops{$item->{itemid}}->{revtime} || 0;
+        $lastmod = $revtime if $revtime > $lastmod;
+
+        # if we don't have a revtime, use the logtime of the item.
+        unless ($revtime) {
+            my $itime = $LJ::EndOfTime - $item->{rlogtime};
+            $lastmod = $itime if $itime > $lastmod;
+        }
+    }
+    $r->set_last_modified($lastmod) if $lastmod;
+
+    # use this $lastmod as the feed's last-modified time
+    # we would've liked to use something like
+    # LJ::get_timeupdate_multi instead, but that only changes
+    # with new updates and doesn't change on edits.
+    $journalinfo->{'modtime'} = $lastmod;
+
+    # regarding $r->set_etag:
+    # http://perl.apache.org/docs/general/correct_headers/correct_headers.html#Entity_Tags
+    # It is strongly recommended that you do not use this method unless you
+    # know what you are doing. set_etag() is expecting to be used in
+    # conjunction with a static request for a file on disk that has been
+    # stat()ed in the course of the current request. It is inappropriate and
+    # "dangerous" to use it for dynamic content.
+
+    # verify that our headers are good; especially check to see if we should
+    # return a 304 (Not Modified) response.
+    if ((my $status = $r->meets_conditions) != $r->OK) {
+        $opts->{handler_return} = $status;
+        return undef;
+    }
+
+    $journalinfo->{email} = $u->email_for_feeds if $u && $u->email_for_feeds;
+
+    # load tags now that we have no chance of jumping out early
+    my $logtags = LJ::Tags::get_logtags($u, \@itemids);
+
+    my %posteru = ();  # map posterids to u objects
+    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @items], [$u]);
+
+    my @cleanitems;
+    my @entries;     # LJ::Entry objects
+
+  ENTRY:
+    foreach my $it (@items)
+    {
+        # load required data
+        my $itemid  = $it->{'itemid'};
+        my $ditemid = $itemid*256 + $it->{'anum'};
+        my $entry_obj = LJ::Entry->new($u, ditemid => $ditemid);
+
+        next ENTRY if $posteru{$it->{'posterid'}} && $posteru{$it->{'posterid'}}->is_suspended;
+        next ENTRY if $entry_obj && $entry_obj->is_suspended_for($remote);
+
+        if ($LJ::UNICODE && $logprops{$itemid}->{'unknown8bit'}) {
+            LJ::item_toutf8($u, \$logtext->{$itemid}->[0],
+                            \$logtext->{$itemid}->[1], $logprops{$itemid});
+        }
+
+        # see if we have a subject and clean it
+        my $subject = $logtext->{$itemid}->[0];
+        if ($subject) {
+            $subject =~ s/[\r\n]/ /g;
+            LJ::CleanHTML::clean_subject_all(\$subject);
+        }
+
+        # an HTML link to the entry. used if we truncate or summarize
+        my $readmore = "<b>(<a href=\"$journalinfo->{link}$ditemid.html\">Read more ...</a>)</b>";
+
+        # empty string so we don't waste time cleaning an entry that won't be used
+        my $event = $u->{'opt_synlevel'} eq 'title' ? '' : $logtext->{$itemid}->[1];
+
+        # clean the event, if non-empty
+        my $ppid = 0;
+        if ($event) {
+
+            # users without 'full_rss' get their logtext bodies truncated
+            # do this now so that the html cleaner will hopefully fix html we break
+            unless ( $u->can_use_full_rss ) {
+                my $trunc = LJ::text_trim($event, 0, 80);
+                $event = "$trunc $readmore" if $trunc ne $event;
+            }
+
+            LJ::CleanHTML::clean_event(\$event,
+                                       {
+                                        wordlength => 0,
+                                        preformatted => $logprops{$itemid}->{opt_preformatted},
+                                        cuturl => $u->{opt_synlevel} eq 'cut' ? "$journalinfo->{link}$ditemid.html" : "",
+                                        to_external_site => 1,
+                                       });
+            # do this after clean so we don't have to about know whether or not
+            # the event is preformatted
+            if ($u->{'opt_synlevel'} eq 'summary') {
+                $event = LJ::Entry->summarize( $event, $readmore );
+            }
+
+            while ($event =~ /<(?:lj-)?poll-(\d+)>/g) {
+                my $pollid = $1;
+
+                my $name = LJ::Poll->new($pollid)->name;
+                if ($name) {
+                    LJ::Poll->clean_poll(\$name);
+                } else {
+                    $name = "#$pollid";
+                }
+
+                $event =~ s!<(lj-)?poll-$pollid>!<div><a href="$LJ::SITEROOT/poll/?id=$pollid">View Poll: $name</a></div>!g;
+            }
+
+            my %args = LJ::parse_args( $r->query_string );
+            LJ::EmbedModule->expand_entry($u, \$event, expand_full => 1)
+                if %args && $args{'unfold_embed'};
+
+            $ppid = $1
+                if $event =~ m!<lj-phonepost journalid=[\'\"]\d+[\'\"] dpid=[\'\"](\d+)[\'\"]( /)?>!;
+        }
+
+        # include comment count image at bottom of event (for readers
+        # that don't understand the commentcount)
+        $event .= "<br /><br />" . $entry_obj->comment_imgtag . " comments";
+
+        my $mood;
+        if ($logprops{$itemid}->{'current_mood'}) {
+            $mood = $logprops{$itemid}->{'current_mood'};
+        } elsif ($logprops{$itemid}->{'current_moodid'}) {
+            $mood = DW::Mood->mood_name( $logprops{$itemid}->{'current_moodid'}+0 );
+        }
+
+        my $createtime = $LJ::EndOfTime - $it->{rlogtime};
+        my $can_comment = ! defined $logprops{$itemid}->{opt_nocomments} ||
+                          ( $logprops{$itemid}->{opt_nocomments} == 0 );
+        my $cleanitem = {
+            itemid     => $itemid,
+            ditemid    => $ditemid,
+            subject    => $subject,
+            event      => $event,
+            createtime => $createtime,
+            eventtime  => $it->{alldatepart},  # ugly: this is of a different format than the other two times.
+            modtime    => $logprops{$itemid}->{revtime} || $createtime,
+            comments   => $can_comment,
+            music      => $logprops{$itemid}->{'current_music'},
+            mood       => $mood,
+            ppid       => $ppid,
+            tags       => [ values %{$logtags->{$itemid} || {}} ],
+            security   => $it->{security},
+            posterid   => $it->{posterid},
+            replycount => $logprops{$itemid}->{'replycount'},
+        };
+        push @cleanitems, $cleanitem;
+        push @entries,    $entry_obj;
+    }
+
+    # fix up the build date to use entry-time
+    $journalinfo->{'builddate'} = LJ::time_to_http($LJ::EndOfTime - $items[0]->{'rlogtime'}),
+
+    return $viewfunc->{handler}->($journalinfo, $u, $opts, \@cleanitems, \@entries);
+}
+
+# helper method to add a namespace to the root of a feed
+sub _add_feed_namespace {
+    my ( $feed, $ns_prefix, $namespace ) = @_;
+    my $doc = $feed->elem->ownerDocument->getDocumentElement;
+    $doc->setAttribute( "xmlns:$ns_prefix", $namespace );
+}
+
+# helper method for create_view_rss and create_view_comments
+sub _init_talkview {
+    my ( $journalinfo, $u, $opts, $talkview ) = @_;
+    my $hubbub = $talkview eq 'rss' && LJ::is_enabled( 'hubbub' );
+    my $ret;
+
+    # header
+    $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
+    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->") . "\n";
+    $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' " .
+            "xmlns:atom10='http://www.w3.org/2005/Atom'>\n";
+
+    # channel attributes
+    my $desc = { rss => LJ::exml( "$journalinfo->{title} - $LJ::SITENAME" ),
+                 comments => "Latest comments in "
+                           . LJ::exml( $journalinfo->{title} ) };
+
+    $ret .= "<channel>\n";
+    $ret .= "  <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
+    $ret .= "  <link>$journalinfo->{link}</link>\n";
+    $ret .= "  <description>" . $desc->{$talkview} . "</description>\n";
+    $ret .= "  <managingEditor>" . LJ::exml($journalinfo->{email}) . "</managingEditor>\n" if $journalinfo->{email};
+    $ret .= "  <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
+    $ret .= "  <generator>LiveJournal / $LJ::SITENAME</generator>\n";
+    $ret .= "  <lj:journal>" . $u->user . "</lj:journal>\n";
+    $ret .= "  <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
+    # TODO: add 'language' field when user.lang has more useful information
+
+    if ( $hubbub ) {
+        $ret .= "  <atom10:link rel='self' href='" . $u->journal_base . "/data/rss' />\n";
+        foreach my $hub (@LJ::HUBBUB_HUBS) {
+            $ret .= "  <atom10:link rel='hub' href='" . LJ::exml($hub) . "' />\n";
+        }
+    }
+
+    ### image block, returns info for their current userpic
+    if ( $u->{'defaultpicid'} ) {
+        my $icon = $u->userpic;
+        my $url = $icon->url;
+        my ( $width, $height ) = $icon->dimensions;
+
+        $ret .= "  <image>\n";
+        $ret .= "    <url>$url</url>\n";
+        $ret .= "    <title>" . LJ::exml( $journalinfo->{title} ) . "</title>\n";
+        $ret .= "    <link>$journalinfo->{link}</link>\n";
+        $ret .= "    <width>$width</width>\n";
+        $ret .= "    <height>$height</height>\n";
+        $ret .= "  </image>\n\n";
+    }
+
+    return $ret;
+}
+
+sub create_view_rss {
+    my ( $journalinfo, $u, $opts, $cleanitems ) = @_;
+
+    my $ret = _init_talkview( $journalinfo, $u, $opts, 'rss' );
+
+    my %posteru = ();  # map posterids to u objects
+    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @$cleanitems], [$u]);
+
+    # output individual item blocks
+
+    foreach my $it (@$cleanitems)
+    {
+        my $itemid = $it->{itemid};
+        my $ditemid = $it->{ditemid};
+        my $poster = $posteru{$it->{posterid}};
+
+        $ret .= "<item>\n";
+        $ret .= "  <guid isPermaLink='true'>$journalinfo->{link}$ditemid.html</guid>\n";
+        $ret .= "  <pubDate>" . LJ::time_to_http($it->{createtime}) . "</pubDate>\n";
+        $ret .= "  <title>" . LJ::exml($it->{subject}) . "</title>\n" if $it->{subject};
+        $ret .= "  <author>" . LJ::exml($journalinfo->{email}) . "</author>" if $journalinfo->{email};
+        $ret .= "  <link>$journalinfo->{link}$ditemid.html</link>\n";
+        # omit the description tag if we're only syndicating titles
+        #   note: the $event was also emptied earlier, in make_feed
+        unless ($u->{'opt_synlevel'} eq 'title') {
+            $ret .= "  <description>" . LJ::exml($it->{event}) . "</description>\n";
+        }
+        if ($it->{comments}) {
+            $ret .= "  <comments>$journalinfo->{link}$ditemid.html</comments>\n";
+        }
+        $ret .= "  <category>$_</category>\n" foreach map { LJ::exml($_) } @{$it->{tags} || []};
+        # support 'podcasting' enclosures
+        $ret .= LJ::Hooks::run_hook( "pp_rss_enclosure",
+                { userid => $u->{userid}, ppid => $it->{ppid} }) if $it->{ppid};
+        # TODO: add author field with posterid's email address, respect communities
+        $ret .= "  <lj:music>" . LJ::exml($it->{music}) . "</lj:music>\n" if $it->{music};
+        $ret .= "  <lj:mood>" . LJ::exml($it->{mood}) . "</lj:mood>\n" if $it->{mood};
+        $ret .= "  <lj:security>" . LJ::exml($it->{security}) . "</lj:security>\n" if $it->{security};
+        $ret .= "  <lj:poster>" . LJ::exml($poster->user) . "</lj:poster>\n" unless $u->equals( $poster );
+        $ret .= "  <lj:reply-count>$it->{replycount}</lj:reply-count>\n";
+        $ret .= "</item>\n";
+    }
+
+    $ret .= "</channel>\n";
+    $ret .= "</rss>\n";
+
+    return $ret;
+}
+
+
+# the creator for the Atom view
+# keys of $opts:
+# single_entry - only output an <entry>..</entry> block. off by default
+# apilinks - output AtomAPI links for posting a new entry or
+#            getting/editing/deleting an existing one. off by default
+sub create_view_atom
+{
+    my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
+    my ( $feed, $xml, $ns, $site_ns_prefix );
+
+    $site_ns_prefix = lc $LJ::SITENAMEABBREV;
+    $ns = "http://www.w3.org/2005/Atom";
+
+    # AtomAPI interface path
+    my $api = $opts->{'apilinks'} ? $u->atom_service_document :
+                                    $u->journal_base . "/data/atom";
+
+    my $make_link = sub {
+        my ( $rel, $type, $href, $title ) = @_;
+        my $link = XML::Atom::Link->new( Version => 1 );
+        $link->rel($rel);
+        $link->type($type) if $type;
+        $link->href($href);
+        $link->title( $title ) if $title;
+        return $link;
+    };
+
+    my $author = XML::Atom::Person->new( Version => 1 );
+    my $journalu = $j->{u};
+    $author->email( $journalu->email_for_feeds ) if $journalu && $journalu->email_for_feeds;
+    $author->name(  $u->{'name'} );
+
+    # feed information
+    unless ($opts->{'single_entry'}) {
+        $feed = XML::Atom::Feed->new( Version => 1 );
+        $xml  = $feed->elem->ownerDocument;
+
+        if ($u->should_block_robots) {
+            _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
+            $xml->getDocumentElement->setAttribute( "idx:index", "no" );
+        }
+
+        $xml->insertBefore( $xml->createComment( LJ::Hooks::run_hook("bot_director") ), $xml->documentElement());
+
+        # attributes
+        $feed->id( $u->atomid );
+        $feed->title( $j->{'title'} || $u->{user} );
+        if ( $j->{'subtitle'} ) {
+            $feed->subtitle( $j->{'subtitle'} );
+        }
+
+        $feed->author( $author );
+        $feed->add_link( $make_link->( 'alternate', 'text/html', $j->{'link'} ) );
+        $feed->add_link(
+            $make_link->(
+                'self',
+                $opts->{'apilinks'}
+                ? ( 'application/atom+xml', "$api/entries" )
+                : ( 'text/xml', $api )
+            )
+        );
+        $feed->updated( LJ::time_to_w3c($j->{'modtime'}, 'Z') );
+
+        my $ljinfo = $xml->createElement( "$site_ns_prefix:journal" );
+        $ljinfo->setAttribute( 'username', LJ::exml($u->user) );
+        $ljinfo->setAttribute( 'type', LJ::exml($u->journaltype_readable) );
+        $xml->getDocumentElement->appendChild( $ljinfo );
+
+        if ( LJ::is_enabled( 'hubbub' ) ) {
+            foreach my $hub (@LJ::HUBBUB_HUBS) {
+                $feed->add_link($make_link->('hub', undef, $hub));
+            }
+        }
+    }
+
+    my $posteru = LJ::load_userids( map { $_->{posterid} } @$cleanitems);
+    # output individual item blocks
+    # FIXME: use LJ::Entry->atom_entry?
+    foreach my $it (@$cleanitems)
+    {
+        my $itemid = $it->{itemid};
+        my $ditemid = $it->{ditemid};
+        my $poster = $posteru->{$it->{posterid}};
+
+        my $entry = XML::Atom::Entry->new( Version => 1 );
+        my $entry_xml = $entry->elem->ownerDocument;
+
+        $entry->id( $u->atomid . ":$ditemid" );
+
+        # author isn't required if it is in the main <feed>
+        # only add author if we are in a single entry view, or
+        # the journal entry isn't owned by the journal. (communities)
+        if ( $opts->{single_entry} || ! $journalu->equals( $poster ) ) {
+            my $author = XML::Atom::Person->new( Version => 1 );
+            $author->email( $poster->email_visible ) if $poster && $poster->email_visible;
+            $author->name(  $poster->{name} );
+            $entry->author( $author );
+
+            # and the lj-specific stuff
+            my $postauthor = $entry_xml->createElement( "$site_ns_prefix:poster" );
+            $postauthor->setAttribute( 'user', LJ::exml($poster->user));
+            $entry_xml->getDocumentElement->appendChild( $postauthor );
+        }
+
+        $entry->add_link(
+            $make_link->( 'alternate', 'text/html', "$j->{'link'}$ditemid.html" )
+        );
+        $entry->add_link(
+            $make_link->( 'self', 'text/xml', "$api/?itemid=$ditemid" )
+        );
+
+        $entry->add_link(
+            $make_link->(
+                'edit',      'application/atom+xml',
+                "$api/entries/$itemid", 'Edit this post'
+            )
+          ) if $opts->{'apilinks'};
+
+        my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime});
+        my $event_date = sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
+                                 $year, $mon, $mday, $hour, $min, $sec);
+
+
+        # title can't be blank and can't be absent, so we have to fake some subject
+        $entry->title( $it->{'subject'} ||
+                       "$journalu->{user} \@ $event_date"
+                       );
+
+
+        $entry->published( LJ::time_to_w3c($it->{createtime}, "Z") );
+        $entry->updated(   LJ::time_to_w3c($it->{modtime},    "Z") );
+
+        foreach my $tag ( @{$it->{tags} || []} ) {
+            my $category = XML::Atom::Category->new( Version => 1 );
+            $category->term( $tag );
+            $entry->add_category( $category );
+        }
+
+        my @currents = ( [ 'music'       => $it->{music}      ],
+                         [ 'mood'        => $it->{mood}       ],
+                         [ 'security'    => $it->{security}   ],
+                         [ 'reply-count' => $it->{replycount} ],
+                       );
+
+        foreach ( @currents ) {
+            my ( $key, $val ) = @$_;
+            if ( defined $val ) {
+                my $elem = $entry_xml->createElement( "$site_ns_prefix:$key" );
+                $elem->appendTextNode( $val );
+                $entry_xml->getDocumentElement->appendChild( $elem );
+            }
+        }
+
+        # if syndicating the complete entry
+        #   -print a content tag
+        # elsif syndicating summaries
+        #   -print a summary tag
+         # else (code omitted), we're syndicating title only
+        #   -print neither (the title has already been printed)
+        #   note: the $event was also emptied earlier, in make_feed
+        #
+        # a lack of a content element is allowed,  as long
+        # as we maintain a proper 'alternate' link (above)
+        my $make_content = sub {
+            my $content = $entry_xml->createElement( $_[0] );
+            $content->setAttribute( 'type', 'html' );
+            $content->setNamespace( $ns );
+            $content->appendTextNode( $it->{'event'} );
+            $entry_xml->getDocumentElement->appendChild( $content );
+        };
+        if ( $u->{'opt_synlevel'} eq 'full' || $u->{'opt_synlevel'} eq 'cut' ) {
+            # Do this manually for now, until XML::Atom supports new
+            # content type classifications.
+            $make_content->('content');
+        } elsif ($u->{'opt_synlevel'} eq 'summary') {
+            $make_content->('summary');
+        }
+
+        if ( $opts->{'single_entry'} ) {
+            _add_feed_namespace( $entry, $site_ns_prefix, $LJ::SITEROOT );
+            return $entry->as_xml;
+        }
+        else {
+            $feed->add_entry( $entry );
+        }
+    }
+
+    _add_feed_namespace( $feed, $site_ns_prefix, $LJ::SITEROOT );
+    return $feed->as_xml;
+}
+
+# create a FOAF page for a user
+sub create_view_foaf {
+    my ($journalinfo, $u, $opts) = @_;
+    my $comm = $u->is_community;
+
+    my $ret;
+
+    # return nothing if we're not a user
+    unless ( $u->is_person || $comm ) {
+        $opts->{handler_return} = 404;
+        return undef;
+    }
+
+    # set our content type
+    $opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset};
+
+    # setup userprops we will need
+    $u->preload_props( qw{
+        aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
+    } );
+
+    # create bare foaf document, for now
+    $ret = "<?xml version='1.0'?>\n";
+    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->");
+    $ret .= "<rdf:RDF\n";
+    $ret .= "   xml:lang=\"en\"\n";
+    $ret .= "   xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
+    $ret .= "   xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n";
+    $ret .= "   xmlns:foaf=\"http://xmlns.com/foaf/0.1/\"\n";
+    $ret .= "   xmlns:ya=\"http://blogs.yandex.ru/schema/foaf/\"\n";
+    $ret .= "   xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"\n";
+    $ret .= "   xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
+
+    # precompute some values
+    my $digest = "";
+    my $remote = LJ::get_remote();
+    if ($u->is_validated) {
+        my $email_visible = $u->email_visible($remote);
+        $digest = Digest::SHA1::sha1_hex("mailto:$email_visible") if $email_visible;
+    }
+
+    # channel attributes
+    $ret .= ($comm ? "  <foaf:Group>\n" : "  <foaf:Person>\n");
+    $ret .= "    <foaf:nick>$u->{user}</foaf:nick>\n";
+    $ret .= "    <foaf:name>". LJ::exml($u->{name}) ."</foaf:name>\n";
+    $ret .= "    <foaf:openid rdf:resource=\"" . $u->journal_base . "/\" />\n"
+        unless $comm;
+
+    # user location
+    if ($u->{'country'}) {
+        my $ecountry = LJ::eurl($u->{'country'});
+        $ret .= "    <ya:country dc:title=\"$ecountry\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry\"/>\n" if $u->can_show_location($remote);
+        if ($u->{'city'}) {
+            my $estate = '';  # FIXME: add state.  Yandex didn't need it.
+            my $ecity = LJ::eurl($u->{'city'});
+            $ret .= "    <ya:city dc:title=\"$ecity\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry&amp;loc_st=$estate&amp;loc_ci=$ecity\"/>\n" if $u->can_show_location($remote);
+       }
+    }
+
+    if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
+        $ret .= "    <foaf:dateOfBirth>".$u->bday_string."</foaf:dateOfBirth>\n";
+    }
+    $ret .= "    <foaf:mbox_sha1sum>$digest</foaf:mbox_sha1sum>\n" if $digest;
+
+    # userpic
+    if (my $picid = $u->{'defaultpicid'}) {
+        $ret .= "    <foaf:img rdf:resource=\"$LJ::USERPIC_ROOT/$picid/$u->{userid}\" />\n";
+    }
+
+    $ret .= "    <foaf:page>\n";
+    $ret .= "      <foaf:Document rdf:about=\"" . $u->profile_url . "\">\n";
+    $ret .= "        <dc:title>$LJ::SITENAME Profile</dc:title>\n";
+    $ret .= "        <dc:description>Full $LJ::SITENAME profile, including information such as interests and bio.</dc:description>\n";
+    $ret .= "      </foaf:Document>\n";
+    $ret .= "    </foaf:page>\n";
+
+    # we want to bail out if they have an external foaf file, because
+    # we want them to be able to provide their own information.
+    if ($u->{external_foaf_url}) {
+        $ret .= "    <rdfs:seeAlso rdf:resource=\"" . LJ::eurl($u->{external_foaf_url}) . "\" />\n";
+        $ret .= ($comm ? "  </foaf:Group>\n" : "  </foaf:Person>\n");
+        $ret .= "</rdf:RDF>\n";
+        return $ret;
+    }
+
+    # contact type information
+    my %types = (
+        aolim => 'aimChatID',
+        icq => 'icqChatID',
+        yahoo => 'yahooChatID',
+        msn => 'msnChatID',
+        jabber => 'jabberID',
+    );
+    if ($u->{allow_contactshow} eq 'Y') {
+        foreach my $type (keys %types) {
+            next unless defined $u->{$type};
+            $ret .= "    <foaf:$types{$type}>" . LJ::exml($u->{$type}) . "</foaf:$types{$type}>\n";
+        }
+    }
+
+    # blog activity
+    {
+        my $count = $u->number_of_posts;
+        $ret .= "    <ya:blogActivity>\n";
+        $ret .= "      <ya:Posts>\n";
+        $ret .= "        <ya:feed rdf:resource=\"" . $u->journal_base ."/data/rss\" dc:type=\"application/rss+xml\" />\n";
+        $ret .= "        <ya:posted>$count</ya:posted>\n";
+        $ret .= "      </ya:Posts>\n";
+        $ret .= "    </ya:blogActivity>\n";
+    }
+
+    # include a user's journal page and web site info
+    $ret .= "    <foaf:weblog rdf:resource=\"" . $u->journal_base . "/\"/>\n";
+    if ($u->{url}) {
+        $ret .= "    <foaf:homepage rdf:resource=\"" . LJ::eurl($u->{url});
+        $ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n";
+    }
+
+    # user bio
+    if ( $u->has_bio ) {
+        my $bio = $u->bio;
+        LJ::text_out( \$bio );
+        LJ::CleanHTML::clean_userbio( \$bio );
+        $ret .= "    <ya:bio>" . LJ::exml( $bio ) . "</ya:bio>\n";
+    }
+
+    # icbm/location info
+    if ($u->{icbm}) {
+        my @loc = split(",", $u->{icbm});
+        $ret .= "    <foaf:based_near><geo:Point geo:lat='" . $loc[0] . "'" .
+                " geo:long='" . $loc[1] . "' /></foaf:based_near>\n";
+    }
+
+    # interests, please!
+    # arrayref of interests rows: [ intid, intname, intcount ]
+    my $intu = $u->get_interests();
+    foreach my $int (@$intu) {
+        LJ::text_out(\$int->[1]); # 1==interest
+        $ret .= "    <foaf:interest dc:title=\"". LJ::exml($int->[1]) . "\" " .
+                "rdf:resource=\"$LJ::SITEROOT/interests?int=" . LJ::eurl($int->[1]) . "\" />\n";
+    }
+
+    # check if the user has a "FOAF-knows" group
+    my $has_foaf_group = $u->trust_groups( name => 'FOAF-knows' ) ? 1 : 0;
+
+    # now information on who you know, limited to a certain maximum number of users
+    my @ids;
+    if ( $has_foaf_group ) {
+        @ids = keys %{ $u->trust_group_members( name => 'FOAF-knows' ) };
+    } else {
+        @ids = $u->trusted_userids;
+    }
+
+    @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS;
+
+    # now load
+    my $users = LJ::load_userids( @ids );
+
+    # iterate to create data structure
+    foreach my $trustid ( @ids ) {
+        next if $trustid == $u->id;
+        my $fu = $users->{$trustid};
+        next if $fu->is_inactive || ! $fu->is_person;
+
+        my $name = LJ::exml($fu->name_raw);
+        my $tagline = LJ::exml($fu->prop('journaltitle') || '');
+        my $upicurl = $fu->userpic ? $fu->userpic->url : '';
+
+        $ret .= $comm ? "    <foaf:member>\n" : "    <foaf:knows>\n";
+        $ret .= "      <foaf:Person>\n";
+        $ret .= "        <foaf:nick>$fu->{'user'}</foaf:nick>\n";
+        $ret .= "        <foaf:member_name>$name</foaf:member_name>\n";
+        $ret .= "        <foaf:tagLine>$tagline</foaf:tagLine>\n";
+        $ret .= "        <foaf:image>$upicurl</foaf:image>\n" if $upicurl;
+        $ret .= "        <rdfs:seeAlso rdf:resource=\"" . $fu->journal_base ."/data/foaf\" />\n";
+        $ret .= "        <foaf:weblog rdf:resource=\"" . $fu->journal_base . "/\"/>\n";
+        $ret .= "      </foaf:Person>\n";
+        $ret .= $comm ? "    </foaf:member>\n" : "    </foaf:knows>\n";
+    }
+
+    # finish off the document
+    $ret .= $comm ? "    </foaf:Group>\n" : "  </foaf:Person>\n";
+    $ret .= "</rdf:RDF>\n";
+
+    return $ret;
+}
+
+# YADIS capability discovery
+sub create_view_yadis {
+    my ($journalinfo, $u, $opts) = @_;
+    my $person = $u->is_person;
+
+    my $ret = "";
+
+    my $println = sub { $ret .= $_[0]."\n"; };
+
+    $println->('<?xml version="1.0" encoding="UTF-8"?>');
+    $println->('<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>');
+
+    local $1;
+    $opts->{pathextra} =~ m!^(/.*)?$!;
+    my $viewchunk = $1;
+
+    my $view;
+    if ($viewchunk eq '') {
+        $view = "recent";
+    }
+    elsif ($viewchunk eq '/read') {
+        $view = "read";
+    }
+    else {
+        $view = '';
+    }
+
+    if ($view eq 'recent') {
+        # Only people (not communities, etc) can be OpenID authenticated
+        if ($person && LJ::OpenID->server_enabled) {
+            $println->('    <Service>');
+            $println->('        <Type>http://openid.net/signon/1.0</Type>');
+            $println->('        <URI>'.LJ::ehtml($LJ::OPENID_SERVER).'</URI>');
+            $println->('    </Service>');
+        }
+    }
+
+    # Local site-specific content
+    # TODO: Give these hooks access to $view somehow?
+    LJ::Hooks::run_hook("yadis_service_descriptors", \$ret);
+
+    $println->('</XRD></xrds:XRDS>');
+    return $ret;
+}
+
+# create a userpic page for a user
+sub create_view_userpics {
+    my ($journalinfo, $u, $opts) = @_;
+    my ( $feed, $xml, $ns );
+
+    $ns = "http://www.w3.org/2005/Atom";
+
+    my $make_link = sub {
+        my ( $rel, $type, $href, $title ) = @_;
+        my $link = XML::Atom::Link->new( Version => 1 );
+        $link->rel($rel);
+        $link->type($type);
+        $link->href($href);
+        $link->title( $title ) if $title;
+        return $link;
+    };
+
+    my $author = XML::Atom::Person->new( Version => 1 );
+    $author->name(  $u->{name} );
+
+    $feed = XML::Atom::Feed->new( Version => 1 );
+    $xml  = $feed->elem->ownerDocument;
+
+    if ($u->should_block_robots) {
+        _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
+        $xml->getDocumentElement->setAttribute( "idx:index", "no" );
+    }
+
+    my $bot = LJ::Hooks::run_hook("bot_director");
+    $xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
+        if $bot;
+
+    $feed->id( $u->atomid . ":userpics" );
+    $feed->title( "$u->{user}'s userpics" );
+
+    $feed->author( $author );
+    $feed->add_link( $make_link->( 'alternate', 'text/html', $u->allpics_base ) );
+    $feed->add_link( $make_link->( 'self', 'text/xml', $u->journal_base() . "/data/userpics" ) );
+
+    # now start building all the userpic data
+    # start up by loading all of our userpic information and creating that part of the feed
+    my $info = $u->get_userpic_info( { load_comments => 1, load_urls => 1, load_descriptions => 1 } );
+
+    my %keywords = ();
+    while (my ($kw, $pic) = each %{$info->{kw}}) {
+        LJ::text_out(\$kw);
+        push @{$keywords{$pic->{picid}}}, LJ::exml($kw);
+    }
+
+    my %comments = ();
+    while (my ($pic, $comment) = each %{$info->{comment}}) {
+        LJ::text_out(\$comment);
+        $comments{$pic} = LJ::strip_html($comment);
+    }
+
+    my %descriptions = ();
+    while ( my( $pic, $description ) = each %{$info->{description}} ) {
+        LJ::text_out(\$description);
+        $descriptions{$pic} = LJ::strip_html($description);
+    }
+
+    my @pics = map { $info->{pic}->{$_} } sort { $a <=> $b }
+               grep { $info->{pic}->{$_}->{state} eq 'N' }
+               keys %{ $info->{pic} };
+
+    # FIXME: It sucks that there are two different methods for aggregating
+    #        the information for a user's set of icons, one of which doesn't
+    #        include keywords and the other of which doesn't include pictime.
+    #        But hey, at least they both use caching.
+
+    my %pictimes = map { $_->picid => $_->pictime }
+                       LJ::Userpic->load_user_userpics( $u );
+
+    my $latest = 0;
+    foreach my $pictime ( values %pictimes ) {
+        $latest = ($latest < $pictime) ? $pictime : $latest;
+    }
+
+    $feed->updated( LJ::time_to_w3c($latest, 'Z') );
+
+    foreach my $pic (@pics) {
+        my $entry = XML::Atom::Entry->new( Version => 1 );
+        my $entry_xml = $entry->elem->ownerDocument;
+
+        $entry->id( $u->atomid . ":userpics:$pic->{picid}" );
+
+        my $title = ($pic->{picid} == $u->{defaultpicid}) ? "default userpic" : "userpic";
+        $entry->title( $title );
+
+        $entry->updated( LJ::time_to_w3c( $pictimes{ $pic->{picid} }, 'Z') );
+
+        my $content;
+        $content = $entry_xml->createElement( "content" );
+        $content->setAttribute( 'src', "$LJ::USERPIC_ROOT/$pic->{picid}/$u->{userid}" );
+        $content->setNamespace( $ns );
+        $entry_xml->getDocumentElement->appendChild( $content );
+
+        foreach my $kw (@{$keywords{$pic->{picid}}}) {
+            my $category = $entry_xml->createElement( 'category' );
+            $category->setAttribute( 'term', $kw );
+            $category->setNamespace( $ns );
+            $entry_xml->getDocumentElement->appendChild( $category );
+        }
+
+        if ( $descriptions{$pic->{picid}} ) {
+            my $content = $entry_xml->createElement( 'title' );
+            $content->setNamespace( $ns );
+            $content->appendTextNode( $descriptions{$pic->{picid}} );
+            $entry_xml->getDocumentElement->appendChild( $content );
+        };
+
+        if($comments{$pic->{picid}}) {
+            my $content = $entry_xml->createElement( "summary" );
+            $content->setNamespace( $ns );
+            $content->appendTextNode( $comments{$pic->{picid}} );
+            $entry_xml->getDocumentElement->appendChild( $content );
+        };
+
+        $feed->add_entry( $entry );
+    }
+
+    return $feed->as_xml;
+}
+
+
+sub create_view_comments {
+    my ( $journalinfo, $u, $opts ) = @_;
+
+    unless ( LJ::is_enabled('latest_comments_rss', $u) ) {
+        $opts->{handler_return} = 404;
+        return 404;
+    }
+
+    unless ( $u->can_use_latest_comments_rss ) {
+        $opts->{handler_return} = 403;
+        return;
+    }
+
+    my $ret = _init_talkview( $journalinfo, $u, $opts, 'comments' );
+
+    my @comments = $u->get_recent_talkitems(25);
+    foreach my $r (@comments)
+    {
+        my $c = LJ::Comment->new($u, jtalkid => $r->{jtalkid});
+        my $thread_url = $c->thread_url;
+        my $subject = $c->subject_raw;
+        LJ::CleanHTML::clean_subject_all(\$subject);
+
+        $ret .= "<item>\n";
+        $ret .= "  <guid isPermaLink='true'>$thread_url</guid>\n";
+        $ret .= "  <pubDate>" . LJ::time_to_http($r->{datepostunix}) . "</pubDate>\n";
+        $ret .= "  <title>" . LJ::exml($subject) . "</title>\n" if $subject;
+        $ret .= "  <link>$thread_url</link>\n";
+        # omit the description tag if we're only syndicating titles
+        unless ($u->{'opt_synlevel'} eq 'title') {
+            my $body = $c->body_raw;
+            LJ::CleanHTML::clean_subject_all(\$body);
+            $ret .= "  <description>" . LJ::exml($body) . "</description>\n";
+        }
+        $ret .= "</item>\n";
+    }
+
+    $ret .= "</channel>\n";
+    $ret .= "</rss>\n";
+
+
+    return $ret;
+}
+
+sub generate_hubbub_jobs {
+    my ( $u, $joblist ) = @_;
+
+    return unless LJ::is_enabled( 'hubbub' );
+
+    foreach my $hub ( @LJ::HUBBUB_HUBS ) {
+        my $make_hubbub_job = sub {
+            my $type = shift;
+
+            my $topic_url = $u->journal_base . "/data/$type";
+            return TheSchwartz::Job->new(
+                funcname => 'TheSchwartz::Worker::PubSubHubbubPublish',
+                arg => {
+                    hub => $hub,
+                    topic_url => $topic_url,
+                },
+                coalesce => $hub,
+            );
+        };
+
+        push @$joblist, $make_hubbub_job->("rss");
+        push @$joblist, $make_hubbub_job->("atom");
+    }
+}
+
+
+1;
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/ljfeed.pl
--- a/cgi-bin/ljfeed.pl	Mon Oct 24 19:56:39 2011 +0800
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1028 +0,0 @@
-#!/usr/bin/perl
-# This code was forked from the LiveJournal project owned and operated
-# by Live Journal, Inc. The code has been modified and expanded by
-# Dreamwidth Studios, LLC. These files were originally licensed under
-# the terms of the license supplied by Live Journal, Inc, which can
-# currently be found at:
-#
-# http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt
-#
-# In accordance with the original license, this code and all its
-# modifications are provided under the GNU General Public License.
-# A copy of that license can be found in the LICENSE file included as
-# part of this distribution.
-
-
-package LJ::Feed;
-use strict;
-
-use LJ::Entry;
-use XML::Atom::Person;
-use XML::Atom::Feed;
-
-my %feedtypes = (
-    rss         => { handler => \&create_view_rss,  need_items => 1 },
-    atom        => { handler => \&create_view_atom, need_items => 1 },
-    foaf        => { handler => \&create_view_foaf,                 },
-    yadis       => { handler => \&create_view_yadis,                },
-    userpics    => { handler => \&create_view_userpics,             },
-    comments    => { handler => \&create_view_comments,             },
-);
-
-sub make_feed
-{
-    my ($r, $u, $remote, $opts) = @_;
-
-    $opts->{pathextra} =~ s!^/(\w+)!!;
-    my $feedtype = $1;
-    my $viewfunc = $feedtypes{$feedtype};
-
-    unless ( $viewfunc && LJ::isu( $u ) ) {
-        $opts->{'handler_return'} = 404;
-        return undef;
-    }
-
-    $r->note('codepath' => "feed.$feedtype") if $r;
-
-    my $dbr = LJ::get_db_reader();
-
-    my $user = $u->user;
-
-    $u->preload_props( qw/ journaltitle journalsubtitle opt_synlevel / );
-
-    LJ::text_out(\$u->{$_})
-        foreach ("name", "url", "urlname");
-
-    # opt_synlevel will default to 'cut'
-    $u->{opt_synlevel} = 'cut'
-        unless $u->{opt_synlevel} &&
-               $u->{opt_synlevel} =~ /^(?:full|cut|summary|title)$/;
-
-    # some data used throughout the channel
-    my $journalinfo = {
-        u         => $u,
-        link      => $u->journal_base . "/",
-        title     => $u->{journaltitle} || $u->name_raw || $u->user,
-        subtitle  => $u->{journalsubtitle} || $u->name_raw,
-        builddate => LJ::time_to_http(time()),
-    };
-
-    # if we do not want items for this view, just call out
-    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
-    return $viewfunc->{handler}->($journalinfo, $u, $opts)
-        unless ($viewfunc->{need_items});
-
-    # for syndicated accounts, redirect to the syndication URL
-    # However, we only want to do this if the data we're returning
-    # is similar. (Not FOAF, for example)
-    if ( $u->is_syndicated ) {
-        my $synurl = $dbr->selectrow_array("SELECT synurl FROM syndicated WHERE userid=$u->{'userid'}");
-        unless ($synurl) {
-            return 'No syndication URL available.';
-        }
-        $opts->{'redir'} = $synurl;
-        return undef;
-    }
-
-    my %FORM = LJ::parse_args( $r->query_string );
-
-    ## load the itemids
-    my (@itemids, @items);
-
-    # for consistency, we call ditemids "itemid" in user-facing settings
-    my $ditemid = defined $FORM{itemid} ? $FORM{itemid} + 0 : 0;
-
-    if ($ditemid) {
-        my $entry = LJ::Entry->new($u, ditemid => $ditemid);
-
-        if (! $entry || ! $entry->valid || ! $entry->visible_to($remote)) {
-            $opts->{'handler_return'} = 404;
-            return undef;
-        }
-
-        @itemids = $entry->jitemid;
-
-        push @items, {
-            itemid => $entry->jitemid,
-            anum => $entry->anum,
-            posterid => $entry->poster->id,
-            security => $entry->security,
-            alldatepart => LJ::alldatepart_s2($entry->eventtime_mysql),
-            rlogtime => $LJ::EndOfTime - LJ::mysqldate_to_time( $entry->logtime_mysql, 0 ),
-        };
-    } else {
-        @items = $u->recent_items(
-            clusterid => $u->{clusterid},
-            clustersource => 'slave',
-            remote => $remote,
-            itemshow => 25,
-            order => 'logtime',
-            tagids => $opts->{tagids},
-            tagmode => $opts->{tagmode},
-            itemids => \@itemids,
-            friendsview => 1,           # this returns rlogtimes
-            dateformat => 'S2',         # S2 format time format is easier
-        );
-    }
-
-    $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
-
-    ### load the log properties
-    my %logprops = ();
-    my $logtext;
-    my $logdb = LJ::get_cluster_reader($u);
-    LJ::load_log_props2($logdb, $u->{'userid'}, \@itemids, \%logprops);
-    $logtext = LJ::get_logtext2($u, @itemids);
-
-    # set last-modified header, then let apache figure out
-    # whether we actually need to send the feed.
-    my $lastmod = 0;
-    foreach my $item (@items) {
-        # revtime of the item.
-        my $revtime = $logprops{$item->{itemid}}->{revtime} || 0;
-        $lastmod = $revtime if $revtime > $lastmod;
-
-        # if we don't have a revtime, use the logtime of the item.
-        unless ($revtime) {
-            my $itime = $LJ::EndOfTime - $item->{rlogtime};
-            $lastmod = $itime if $itime > $lastmod;
-        }
-    }
-    $r->set_last_modified($lastmod) if $lastmod;
-
-    # use this $lastmod as the feed's last-modified time
-    # we would've liked to use something like
-    # LJ::get_timeupdate_multi instead, but that only changes
-    # with new updates and doesn't change on edits.
-    $journalinfo->{'modtime'} = $lastmod;
-
-    # regarding $r->set_etag:
-    # http://perl.apache.org/docs/general/correct_headers/correct_headers.html#Entity_Tags
-    # It is strongly recommended that you do not use this method unless you
-    # know what you are doing. set_etag() is expecting to be used in
-    # conjunction with a static request for a file on disk that has been
-    # stat()ed in the course of the current request. It is inappropriate and
-    # "dangerous" to use it for dynamic content.
-
-    # verify that our headers are good; especially check to see if we should
-    # return a 304 (Not Modified) response.
-    if ((my $status = $r->meets_conditions) != $r->OK) {
-        $opts->{handler_return} = $status;
-        return undef;
-    }
-
-    $journalinfo->{email} = $u->email_for_feeds if $u && $u->email_for_feeds;
-
-    # load tags now that we have no chance of jumping out early
-    my $logtags = LJ::Tags::get_logtags($u, \@itemids);
-
-    my %posteru = ();  # map posterids to u objects
-    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @items], [$u]);
-
-    my @cleanitems;
-    my @entries;     # LJ::Entry objects
-
-  ENTRY:
-    foreach my $it (@items)
-    {
-        # load required data
-        my $itemid  = $it->{'itemid'};
-        my $ditemid = $itemid*256 + $it->{'anum'};
-        my $entry_obj = LJ::Entry->new($u, ditemid => $ditemid);
-
-        next ENTRY if $posteru{$it->{'posterid'}} && $posteru{$it->{'posterid'}}->is_suspended;
-        next ENTRY if $entry_obj && $entry_obj->is_suspended_for($remote);
-
-        if ($LJ::UNICODE && $logprops{$itemid}->{'unknown8bit'}) {
-            LJ::item_toutf8($u, \$logtext->{$itemid}->[0],
-                            \$logtext->{$itemid}->[1], $logprops{$itemid});
-        }
-
-        # see if we have a subject and clean it
-        my $subject = $logtext->{$itemid}->[0];
-        if ($subject) {
-            $subject =~ s/[\r\n]/ /g;
-            LJ::CleanHTML::clean_subject_all(\$subject);
-        }
-
-        # an HTML link to the entry. used if we truncate or summarize
-        my $readmore = "<b>(<a href=\"$journalinfo->{link}$ditemid.html\">Read more ...</a>)</b>";
-
-        # empty string so we don't waste time cleaning an entry that won't be used
-        my $event = $u->{'opt_synlevel'} eq 'title' ? '' : $logtext->{$itemid}->[1];
-
-        # clean the event, if non-empty
-        my $ppid = 0;
-        if ($event) {
-
-            # users without 'full_rss' get their logtext bodies truncated
-            # do this now so that the html cleaner will hopefully fix html we break
-            unless ( $u->can_use_full_rss ) {
-                my $trunc = LJ::text_trim($event, 0, 80);
-                $event = "$trunc $readmore" if $trunc ne $event;
-            }
-
-            LJ::CleanHTML::clean_event(\$event,
-                                       {
-                                        wordlength => 0,
-                                        preformatted => $logprops{$itemid}->{opt_preformatted},
-                                        cuturl => $u->{opt_synlevel} eq 'cut' ? "$journalinfo->{link}$ditemid.html" : "",
-                                        to_external_site => 1,
-                                       });
-            # do this after clean so we don't have to about know whether or not
-            # the event is preformatted
-            if ($u->{'opt_synlevel'} eq 'summary') {
-                $event = LJ::Entry->summarize( $event, $readmore );
-            }
-
-            while ($event =~ /<(?:lj-)?poll-(\d+)>/g) {
-                my $pollid = $1;
-
-                my $name = LJ::Poll->new($pollid)->name;
-                if ($name) {
-                    LJ::Poll->clean_poll(\$name);
-                } else {
-                    $name = "#$pollid";
-                }
-
-                $event =~ s!<(lj-)?poll-$pollid>!<div><a href="$LJ::SITEROOT/poll/?id=$pollid">View Poll: $name</a></div>!g;
-            }
-
-            my %args = LJ::parse_args( $r->query_string );
-            LJ::EmbedModule->expand_entry($u, \$event, expand_full => 1)
-                if %args && $args{'unfold_embed'};
-
-            $ppid = $1
-                if $event =~ m!<lj-phonepost journalid=[\'\"]\d+[\'\"] dpid=[\'\"](\d+)[\'\"]( /)?>!;
-        }
-
-        # include comment count image at bottom of event (for readers
-        # that don't understand the commentcount)
-        $event .= "<br /><br />" . $entry_obj->comment_imgtag . " comments";
-
-        my $mood;
-        if ($logprops{$itemid}->{'current_mood'}) {
-            $mood = $logprops{$itemid}->{'current_mood'};
-        } elsif ($logprops{$itemid}->{'current_moodid'}) {
-            $mood = DW::Mood->mood_name( $logprops{$itemid}->{'current_moodid'}+0 );
-        }
-
-        my $createtime = $LJ::EndOfTime - $it->{rlogtime};
-        my $can_comment = ! defined $logprops{$itemid}->{opt_nocomments} ||
-                          ( $logprops{$itemid}->{opt_nocomments} == 0 );
-        my $cleanitem = {
-            itemid     => $itemid,
-            ditemid    => $ditemid,
-            subject    => $subject,
-            event      => $event,
-            createtime => $createtime,
-            eventtime  => $it->{alldatepart},  # ugly: this is of a different format than the other two times.
-            modtime    => $logprops{$itemid}->{revtime} || $createtime,
-            comments   => $can_comment,
-            music      => $logprops{$itemid}->{'current_music'},
-            mood       => $mood,
-            ppid       => $ppid,
-            tags       => [ values %{$logtags->{$itemid} || {}} ],
-            security   => $it->{security},
-            posterid   => $it->{posterid},
-            replycount => $logprops{$itemid}->{'replycount'},
-        };
-        push @cleanitems, $cleanitem;
-        push @entries,    $entry_obj;
-    }
-
-    # fix up the build date to use entry-time
-    $journalinfo->{'builddate'} = LJ::time_to_http($LJ::EndOfTime - $items[0]->{'rlogtime'}),
-
-    return $viewfunc->{handler}->($journalinfo, $u, $opts, \@cleanitems, \@entries);
-}
-
-# helper method to add a namespace to the root of a feed
-sub _add_feed_namespace {
-    my ( $feed, $ns_prefix, $namespace ) = @_;
-    my $doc = $feed->elem->ownerDocument->getDocumentElement;
-    $doc->setAttribute( "xmlns:$ns_prefix", $namespace );
-}
-
-# helper method for create_view_rss and create_view_comments
-sub _init_talkview {
-    my ( $journalinfo, $u, $opts, $talkview ) = @_;
-    my $hubbub = $talkview eq 'rss' && LJ::is_enabled( 'hubbub' );
-    my $ret;
-
-    # header
-    $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
-    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->") . "\n";
-    $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' " .
-            "xmlns:atom10='http://www.w3.org/2005/Atom'>\n";
-
-    # channel attributes
-    my $desc = { rss => LJ::exml( "$journalinfo->{title} - $LJ::SITENAME" ),
-                 comments => "Latest comments in "
-                           . LJ::exml( $journalinfo->{title} ) };
-
-    $ret .= "<channel>\n";
-    $ret .= "  <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
-    $ret .= "  <link>$journalinfo->{link}</link>\n";
-    $ret .= "  <description>" . $desc->{$talkview} . "</description>\n";
-    $ret .= "  <managingEditor>" . LJ::exml($journalinfo->{email}) . "</managingEditor>\n" if $journalinfo->{email};
-    $ret .= "  <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
-    $ret .= "  <generator>LiveJournal / $LJ::SITENAME</generator>\n";
-    $ret .= "  <lj:journal>" . $u->user . "</lj:journal>\n";
-    $ret .= "  <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
-    # TODO: add 'language' field when user.lang has more useful information
-
-    if ( $hubbub ) {
-        $ret .= "  <atom10:link rel='self' href='" . $u->journal_base . "/data/rss' />\n";
-        foreach my $hub (@LJ::HUBBUB_HUBS) {
-            $ret .= "  <atom10:link rel='hub' href='" . LJ::exml($hub) . "' />\n";
-        }
-    }
-
-    ### image block, returns info for their current userpic
-    if ( $u->{'defaultpicid'} ) {
-        my $icon = $u->userpic;
-        my $url = $icon->url;
-        my ( $width, $height ) = $icon->dimensions;
-
-        $ret .= "  <image>\n";
-        $ret .= "    <url>$url</url>\n";
-        $ret .= "    <title>" . LJ::exml( $journalinfo->{title} ) . "</title>\n";
-        $ret .= "    <link>$journalinfo->{link}</link>\n";
-        $ret .= "    <width>$width</width>\n";
-        $ret .= "    <height>$height</height>\n";
-        $ret .= "  </image>\n\n";
-    }
-
-    return $ret;
-}
-
-sub create_view_rss {
-    my ( $journalinfo, $u, $opts, $cleanitems ) = @_;
-
-    my $ret = _init_talkview( $journalinfo, $u, $opts, 'rss' );
-
-    my %posteru = ();  # map posterids to u objects
-    LJ::load_userids_multiple([map { $_->{'posterid'}, \$posteru{$_->{'posterid'}} } @$cleanitems], [$u]);
-
-    # output individual item blocks
-
-    foreach my $it (@$cleanitems)
-    {
-        my $itemid = $it->{itemid};
-        my $ditemid = $it->{ditemid};
-        my $poster = $posteru{$it->{posterid}};
-
-        $ret .= "<item>\n";
-        $ret .= "  <guid isPermaLink='true'>$journalinfo->{link}$ditemid.html</guid>\n";
-        $ret .= "  <pubDate>" . LJ::time_to_http($it->{createtime}) . "</pubDate>\n";
-        $ret .= "  <title>" . LJ::exml($it->{subject}) . "</title>\n" if $it->{subject};
-        $ret .= "  <author>" . LJ::exml($journalinfo->{email}) . "</author>" if $journalinfo->{email};
-        $ret .= "  <link>$journalinfo->{link}$ditemid.html</link>\n";
-        # omit the description tag if we're only syndicating titles
-        #   note: the $event was also emptied earlier, in make_feed
-        unless ($u->{'opt_synlevel'} eq 'title') {
-            $ret .= "  <description>" . LJ::exml($it->{event}) . "</description>\n";
-        }
-        if ($it->{comments}) {
-            $ret .= "  <comments>$journalinfo->{link}$ditemid.html</comments>\n";
-        }
-        $ret .= "  <category>$_</category>\n" foreach map { LJ::exml($_) } @{$it->{tags} || []};
-        # support 'podcasting' enclosures
-        $ret .= LJ::Hooks::run_hook( "pp_rss_enclosure",
-                { userid => $u->{userid}, ppid => $it->{ppid} }) if $it->{ppid};
-        # TODO: add author field with posterid's email address, respect communities
-        $ret .= "  <lj:music>" . LJ::exml($it->{music}) . "</lj:music>\n" if $it->{music};
-        $ret .= "  <lj:mood>" . LJ::exml($it->{mood}) . "</lj:mood>\n" if $it->{mood};
-        $ret .= "  <lj:security>" . LJ::exml($it->{security}) . "</lj:security>\n" if $it->{security};
-        $ret .= "  <lj:poster>" . LJ::exml($poster->user) . "</lj:poster>\n" unless $u->equals( $poster );
-        $ret .= "  <lj:reply-count>$it->{replycount}</lj:reply-count>\n";
-        $ret .= "</item>\n";
-    }
-
-    $ret .= "</channel>\n";
-    $ret .= "</rss>\n";
-
-    return $ret;
-}
-
-
-# the creator for the Atom view
-# keys of $opts:
-# single_entry - only output an <entry>..</entry> block. off by default
-# apilinks - output AtomAPI links for posting a new entry or
-#            getting/editing/deleting an existing one. off by default
-sub create_view_atom
-{
-    my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
-    my ( $feed, $xml, $ns, $site_ns_prefix );
-
-    $site_ns_prefix = lc $LJ::SITENAMEABBREV;
-    $ns = "http://www.w3.org/2005/Atom";
-
-    # AtomAPI interface path
-    my $api = $opts->{'apilinks'} ? $u->atom_service_document :
-                                    $u->journal_base . "/data/atom";
-
-    my $make_link = sub {
-        my ( $rel, $type, $href, $title ) = @_;
-        my $link = XML::Atom::Link->new( Version => 1 );
-        $link->rel($rel);
-        $link->type($type) if $type;
-        $link->href($href);
-        $link->title( $title ) if $title;
-        return $link;
-    };
-
-    my $author = XML::Atom::Person->new( Version => 1 );
-    my $journalu = $j->{u};
-    $author->email( $journalu->email_for_feeds ) if $journalu && $journalu->email_for_feeds;
-    $author->name(  $u->{'name'} );
-
-    # feed information
-    unless ($opts->{'single_entry'}) {
-        $feed = XML::Atom::Feed->new( Version => 1 );
-        $xml  = $feed->elem->ownerDocument;
-
-        if ($u->should_block_robots) {
-            _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
-            $xml->getDocumentElement->setAttribute( "idx:index", "no" );
-        }
-
-        $xml->insertBefore( $xml->createComment( LJ::Hooks::run_hook("bot_director") ), $xml->documentElement());
-
-        # attributes
-        $feed->id( $u->atomid );
-        $feed->title( $j->{'title'} || $u->{user} );
-        if ( $j->{'subtitle'} ) {
-            $feed->subtitle( $j->{'subtitle'} );
-        }
-
-        $feed->author( $author );
-        $feed->add_link( $make_link->( 'alternate', 'text/html', $j->{'link'} ) );
-        $feed->add_link(
-            $make_link->(
-                'self',
-                $opts->{'apilinks'}
-                ? ( 'application/atom+xml', "$api/entries" )
-                : ( 'text/xml', $api )
-            )
-        );
-        $feed->updated( LJ::time_to_w3c($j->{'modtime'}, 'Z') );
-
-        my $ljinfo = $xml->createElement( "$site_ns_prefix:journal" );
-        $ljinfo->setAttribute( 'username', LJ::exml($u->user) );
-        $ljinfo->setAttribute( 'type', LJ::exml($u->journaltype_readable) );
-        $xml->getDocumentElement->appendChild( $ljinfo );
-
-        if ( LJ::is_enabled( 'hubbub' ) ) {
-            foreach my $hub (@LJ::HUBBUB_HUBS) {
-                $feed->add_link($make_link->('hub', undef, $hub));
-            }
-        }
-    }
-
-    my $posteru = LJ::load_userids( map { $_->{posterid} } @$cleanitems);
-    # output individual item blocks
-    # FIXME: use LJ::Entry->atom_entry?
-    foreach my $it (@$cleanitems)
-    {
-        my $itemid = $it->{itemid};
-        my $ditemid = $it->{ditemid};
-        my $poster = $posteru->{$it->{posterid}};
-
-        my $entry = XML::Atom::Entry->new( Version => 1 );
-        my $entry_xml = $entry->elem->ownerDocument;
-
-        $entry->id( $u->atomid . ":$ditemid" );
-
-        # author isn't required if it is in the main <feed>
-        # only add author if we are in a single entry view, or
-        # the journal entry isn't owned by the journal. (communities)
-        if ( $opts->{single_entry} || ! $journalu->equals( $poster ) ) {
-            my $author = XML::Atom::Person->new( Version => 1 );
-            $author->email( $poster->email_visible ) if $poster && $poster->email_visible;
-            $author->name(  $poster->{name} );
-            $entry->author( $author );
-
-            # and the lj-specific stuff
-            my $postauthor = $entry_xml->createElement( "$site_ns_prefix:poster" );
-            $postauthor->setAttribute( 'user', LJ::exml($poster->user));
-            $entry_xml->getDocumentElement->appendChild( $postauthor );
-        }
-
-        $entry->add_link(
-            $make_link->( 'alternate', 'text/html', "$j->{'link'}$ditemid.html" )
-        );
-        $entry->add_link(
-            $make_link->( 'self', 'text/xml', "$api/?itemid=$ditemid" )
-        );
-
-        $entry->add_link(
-            $make_link->(
-                'edit',      'application/atom+xml',
-                "$api/entries/$itemid", 'Edit this post'
-            )
-          ) if $opts->{'apilinks'};
-
-        my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime});
-        my $event_date = sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
-                                 $year, $mon, $mday, $hour, $min, $sec);
-
-
-        # title can't be blank and can't be absent, so we have to fake some subject
-        $entry->title( $it->{'subject'} ||
-                       "$journalu->{user} \@ $event_date"
-                       );
-
-
-        $entry->published( LJ::time_to_w3c($it->{createtime}, "Z") );
-        $entry->updated(   LJ::time_to_w3c($it->{modtime},    "Z") );
-
-        foreach my $tag ( @{$it->{tags} || []} ) {
-            my $category = XML::Atom::Category->new( Version => 1 );
-            $category->term( $tag );
-            $entry->add_category( $category );
-        }
-
-        my @currents = ( [ 'music'       => $it->{music}      ],
-                         [ 'mood'        => $it->{mood}       ],
-                         [ 'security'    => $it->{security}   ],
-                         [ 'reply-count' => $it->{replycount} ],
-                       );
-
-        foreach ( @currents ) {
-            my ( $key, $val ) = @$_;
-            if ( defined $val ) {
-                my $elem = $entry_xml->createElement( "$site_ns_prefix:$key" );
-                $elem->appendTextNode( $val );
-                $entry_xml->getDocumentElement->appendChild( $elem );
-            }
-        }
-
-        # if syndicating the complete entry
-        #   -print a content tag
-        # elsif syndicating summaries
-        #   -print a summary tag
-         # else (code omitted), we're syndicating title only
-        #   -print neither (the title has already been printed)
-        #   note: the $event was also emptied earlier, in make_feed
-        #
-        # a lack of a content element is allowed,  as long
-        # as we maintain a proper 'alternate' link (above)
-        my $make_content = sub {
-            my $content = $entry_xml->createElement( $_[0] );
-            $content->setAttribute( 'type', 'html' );
-            $content->setNamespace( $ns );
-            $content->appendTextNode( $it->{'event'} );
-            $entry_xml->getDocumentElement->appendChild( $content );
-        };
-        if ( $u->{'opt_synlevel'} eq 'full' || $u->{'opt_synlevel'} eq 'cut' ) {
-            # Do this manually for now, until XML::Atom supports new
-            # content type classifications.
-            $make_content->('content');
-        } elsif ($u->{'opt_synlevel'} eq 'summary') {
-            $make_content->('summary');
-        }
-
-        if ( $opts->{'single_entry'} ) {
-            _add_feed_namespace( $entry, $site_ns_prefix, $LJ::SITEROOT );
-            return $entry->as_xml;
-        }
-        else {
-            $feed->add_entry( $entry );
-        }
-    }
-
-    _add_feed_namespace( $feed, $site_ns_prefix, $LJ::SITEROOT );
-    return $feed->as_xml;
-}
-
-# create a FOAF page for a user
-sub create_view_foaf {
-    my ($journalinfo, $u, $opts) = @_;
-    my $comm = $u->is_community;
-
-    my $ret;
-
-    # return nothing if we're not a user
-    unless ( $u->is_person || $comm ) {
-        $opts->{handler_return} = 404;
-        return undef;
-    }
-
-    # set our content type
-    $opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset};
-
-    # setup userprops we will need
-    $u->preload_props( qw{
-        aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
-    } );
-
-    # create bare foaf document, for now
-    $ret = "<?xml version='1.0'?>\n";
-    $ret .= LJ::Hooks::run_hook("bot_director", "<!-- ", " -->");
-    $ret .= "<rdf:RDF\n";
-    $ret .= "   xml:lang=\"en\"\n";
-    $ret .= "   xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
-    $ret .= "   xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n";
-    $ret .= "   xmlns:foaf=\"http://xmlns.com/foaf/0.1/\"\n";
-    $ret .= "   xmlns:ya=\"http://blogs.yandex.ru/schema/foaf/\"\n";
-    $ret .= "   xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"\n";
-    $ret .= "   xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
-
-    # precompute some values
-    my $digest = "";
-    my $remote = LJ::get_remote();
-    if ($u->is_validated) {
-        my $email_visible = $u->email_visible($remote);
-        $digest = Digest::SHA1::sha1_hex("mailto:$email_visible") if $email_visible;
-    }
-
-    # channel attributes
-    $ret .= ($comm ? "  <foaf:Group>\n" : "  <foaf:Person>\n");
-    $ret .= "    <foaf:nick>$u->{user}</foaf:nick>\n";
-    $ret .= "    <foaf:name>". LJ::exml($u->{name}) ."</foaf:name>\n";
-    $ret .= "    <foaf:openid rdf:resource=\"" . $u->journal_base . "/\" />\n"
-        unless $comm;
-
-    # user location
-    if ($u->{'country'}) {
-        my $ecountry = LJ::eurl($u->{'country'});
-        $ret .= "    <ya:country dc:title=\"$ecountry\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry\"/>\n" if $u->can_show_location($remote);
-        if ($u->{'city'}) {
-            my $estate = '';  # FIXME: add state.  Yandex didn't need it.
-            my $ecity = LJ::eurl($u->{'city'});
-            $ret .= "    <ya:city dc:title=\"$ecity\" rdf:resource=\"$LJ::SITEROOT/directorysearch?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry&amp;loc_st=$estate&amp;loc_ci=$ecity\"/>\n" if $u->can_show_location($remote);
-       }
-    }
-
-    if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
-        $ret .= "    <foaf:dateOfBirth>".$u->bday_string."</foaf:dateOfBirth>\n";
-    }
-    $ret .= "    <foaf:mbox_sha1sum>$digest</foaf:mbox_sha1sum>\n" if $digest;
-
-    # userpic
-    if (my $picid = $u->{'defaultpicid'}) {
-        $ret .= "    <foaf:img rdf:resource=\"$LJ::USERPIC_ROOT/$picid/$u->{userid}\" />\n";
-    }
-
-    $ret .= "    <foaf:page>\n";
-    $ret .= "      <foaf:Document rdf:about=\"" . $u->profile_url . "\">\n";
-    $ret .= "        <dc:title>$LJ::SITENAME Profile</dc:title>\n";
-    $ret .= "        <dc:description>Full $LJ::SITENAME profile, including information such as interests and bio.</dc:description>\n";
-    $ret .= "      </foaf:Document>\n";
-    $ret .= "    </foaf:page>\n";
-
-    # we want to bail out if they have an external foaf file, because
-    # we want them to be able to provide their own information.
-    if ($u->{external_foaf_url}) {
-        $ret .= "    <rdfs:seeAlso rdf:resource=\"" . LJ::eurl($u->{external_foaf_url}) . "\" />\n";
-        $ret .= ($comm ? "  </foaf:Group>\n" : "  </foaf:Person>\n");
-        $ret .= "</rdf:RDF>\n";
-        return $ret;
-    }
-
-    # contact type information
-    my %types = (
-        aolim => 'aimChatID',
-        icq => 'icqChatID',
-        yahoo => 'yahooChatID',
-        msn => 'msnChatID',
-        jabber => 'jabberID',
-    );
-    if ($u->{allow_contactshow} eq 'Y') {
-        foreach my $type (keys %types) {
-            next unless defined $u->{$type};
-            $ret .= "    <foaf:$types{$type}>" . LJ::exml($u->{$type}) . "</foaf:$types{$type}>\n";
-        }
-    }
-
-    # blog activity
-    {
-        my $count = $u->number_of_posts;
-        $ret .= "    <ya:blogActivity>\n";
-        $ret .= "      <ya:Posts>\n";
-        $ret .= "        <ya:feed rdf:resource=\"" . $u->journal_base ."/data/rss\" dc:type=\"application/rss+xml\" />\n";
-        $ret .= "        <ya:posted>$count</ya:posted>\n";
-        $ret .= "      </ya:Posts>\n";
-        $ret .= "    </ya:blogActivity>\n";
-    }
-
-    # include a user's journal page and web site info
-    $ret .= "    <foaf:weblog rdf:resource=\"" . $u->journal_base . "/\"/>\n";
-    if ($u->{url}) {
-        $ret .= "    <foaf:homepage rdf:resource=\"" . LJ::eurl($u->{url});
-        $ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n";
-    }
-
-    # user bio
-    if ( $u->has_bio ) {
-        my $bio = $u->bio;
-        LJ::text_out( \$bio );
-        LJ::CleanHTML::clean_userbio( \$bio );
-        $ret .= "    <ya:bio>" . LJ::exml( $bio ) . "</ya:bio>\n";
-    }
-
-    # icbm/location info
-    if ($u->{icbm}) {
-        my @loc = split(",", $u->{icbm});
-        $ret .= "    <foaf:based_near><geo:Point geo:lat='" . $loc[0] . "'" .
-                " geo:long='" . $loc[1] . "' /></foaf:based_near>\n";
-    }
-
-    # interests, please!
-    # arrayref of interests rows: [ intid, intname, intcount ]
-    my $intu = $u->get_interests();
-    foreach my $int (@$intu) {
-        LJ::text_out(\$int->[1]); # 1==interest
-        $ret .= "    <foaf:interest dc:title=\"". LJ::exml($int->[1]) . "\" " .
-                "rdf:resource=\"$LJ::SITEROOT/interests?int=" . LJ::eurl($int->[1]) . "\" />\n";
-    }
-
-    # check if the user has a "FOAF-knows" group
-    my $has_foaf_group = $u->trust_groups( name => 'FOAF-knows' ) ? 1 : 0;
-
-    # now information on who you know, limited to a certain maximum number of users
-    my @ids;
-    if ( $has_foaf_group ) {
-        @ids = keys %{ $u->trust_group_members( name => 'FOAF-knows' ) };
-    } else {
-        @ids = $u->trusted_userids;
-    }
-
-    @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS;
-
-    # now load
-    my $users = LJ::load_userids( @ids );
-
-    # iterate to create data structure
-    foreach my $trustid ( @ids ) {
-        next if $trustid == $u->id;
-        my $fu = $users->{$trustid};
-        next if $fu->is_inactive || ! $fu->is_person;
-
-        my $name = LJ::exml($fu->name_raw);
-        my $tagline = LJ::exml($fu->prop('journaltitle') || '');
-        my $upicurl = $fu->userpic ? $fu->userpic->url : '';
-
-        $ret .= $comm ? "    <foaf:member>\n" : "    <foaf:knows>\n";
-        $ret .= "      <foaf:Person>\n";
-        $ret .= "        <foaf:nick>$fu->{'user'}</foaf:nick>\n";
-        $ret .= "        <foaf:member_name>$name</foaf:member_name>\n";
-        $ret .= "        <foaf:tagLine>$tagline</foaf:tagLine>\n";
-        $ret .= "        <foaf:image>$upicurl</foaf:image>\n" if $upicurl;
-        $ret .= "        <rdfs:seeAlso rdf:resource=\"" . $fu->journal_base ."/data/foaf\" />\n";
-        $ret .= "        <foaf:weblog rdf:resource=\"" . $fu->journal_base . "/\"/>\n";
-        $ret .= "      </foaf:Person>\n";
-        $ret .= $comm ? "    </foaf:member>\n" : "    </foaf:knows>\n";
-    }
-
-    # finish off the document
-    $ret .= $comm ? "    </foaf:Group>\n" : "  </foaf:Person>\n";
-    $ret .= "</rdf:RDF>\n";
-
-    return $ret;
-}
-
-# YADIS capability discovery
-sub create_view_yadis {
-    my ($journalinfo, $u, $opts) = @_;
-    my $person = $u->is_person;
-
-    my $ret = "";
-
-    my $println = sub { $ret .= $_[0]."\n"; };
-
-    $println->('<?xml version="1.0" encoding="UTF-8"?>');
-    $println->('<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>');
-
-    local $1;
-    $opts->{pathextra} =~ m!^(/.*)?$!;
-    my $viewchunk = $1;
-
-    my $view;
-    if ($viewchunk eq '') {
-        $view = "recent";
-    }
-    elsif ($viewchunk eq '/read') {
-        $view = "read";
-    }
-    else {
-        $view = '';
-    }
-
-    if ($view eq 'recent') {
-        # Only people (not communities, etc) can be OpenID authenticated
-        if ($person && LJ::OpenID->server_enabled) {
-            $println->('    <Service>');
-            $println->('        <Type>http://openid.net/signon/1.0</Type>');
-            $println->('        <URI>'.LJ::ehtml($LJ::OPENID_SERVER).'</URI>');
-            $println->('    </Service>');
-        }
-    }
-
-    # Local site-specific content
-    # TODO: Give these hooks access to $view somehow?
-    LJ::Hooks::run_hook("yadis_service_descriptors", \$ret);
-
-    $println->('</XRD></xrds:XRDS>');
-    return $ret;
-}
-
-# create a userpic page for a user
-sub create_view_userpics {
-    my ($journalinfo, $u, $opts) = @_;
-    my ( $feed, $xml, $ns );
-
-    $ns = "http://www.w3.org/2005/Atom";
-
-    my $make_link = sub {
-        my ( $rel, $type, $href, $title ) = @_;
-        my $link = XML::Atom::Link->new( Version => 1 );
-        $link->rel($rel);
-        $link->type($type);
-        $link->href($href);
-        $link->title( $title ) if $title;
-        return $link;
-    };
-
-    my $author = XML::Atom::Person->new( Version => 1 );
-    $author->name(  $u->{name} );
-
-    $feed = XML::Atom::Feed->new( Version => 1 );
-    $xml  = $feed->elem->ownerDocument;
-
-    if ($u->should_block_robots) {
-        _add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
-        $xml->getDocumentElement->setAttribute( "idx:index", "no" );
-    }
-
-    my $bot = LJ::Hooks::run_hook("bot_director");
-    $xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
-        if $bot;
-
-    $feed->id( $u->atomid . ":userpics" );
-    $feed->title( "$u->{user}'s userpics" );
-
-    $feed->author( $author );
-    $feed->add_link( $make_link->( 'alternate', 'text/html', $u->allpics_base ) );
-    $feed->add_link( $make_link->( 'self', 'text/xml', $u->journal_base() . "/data/userpics" ) );
-
-    # now start building all the userpic data
-    # start up by loading all of our userpic information and creating that part of the feed
-    my $info = $u->get_userpic_info( { load_comments => 1, load_urls => 1, load_descriptions => 1 } );
-
-    my %keywords = ();
-    while (my ($kw, $pic) = each %{$info->{kw}}) {
-        LJ::text_out(\$kw);
-        push @{$keywords{$pic->{picid}}}, LJ::exml($kw);
-    }
-
-    my %comments = ();
-    while (my ($pic, $comment) = each %{$info->{comment}}) {
-        LJ::text_out(\$comment);
-        $comments{$pic} = LJ::strip_html($comment);
-    }
-
-    my %descriptions = ();
-    while ( my( $pic, $description ) = each %{$info->{description}} ) {
-        LJ::text_out(\$description);
-        $descriptions{$pic} = LJ::strip_html($description);
-    }
-
-    my @pics = map { $info->{pic}->{$_} } sort { $a <=> $b }
-               grep { $info->{pic}->{$_}->{state} eq 'N' }
-               keys %{ $info->{pic} };
-
-    # FIXME: It sucks that there are two different methods for aggregating
-    #        the information for a user's set of icons, one of which doesn't
-    #        include keywords and the other of which doesn't include pictime.
-    #        But hey, at least they both use caching.
-
-    my %pictimes = map { $_->picid => $_->pictime }
-                       LJ::Userpic->load_user_userpics( $u );
-
-    my $latest = 0;
-    foreach my $pictime ( values %pictimes ) {
-        $latest = ($latest < $pictime) ? $pictime : $latest;
-    }
-
-    $feed->updated( LJ::time_to_w3c($latest, 'Z') );
-
-    foreach my $pic (@pics) {
-        my $entry = XML::Atom::Entry->new( Version => 1 );
-        my $entry_xml = $entry->elem->ownerDocument;
-
-        $entry->id( $u->atomid . ":userpics:$pic->{picid}" );
-
-        my $title = ($pic->{picid} == $u->{defaultpicid}) ? "default userpic" : "userpic";
-        $entry->title( $title );
-
-        $entry->updated( LJ::time_to_w3c( $pictimes{ $pic->{picid} }, 'Z') );
-
-        my $content;
-        $content = $entry_xml->createElement( "content" );
-        $content->setAttribute( 'src', "$LJ::USERPIC_ROOT/$pic->{picid}/$u->{userid}" );
-        $content->setNamespace( $ns );
-        $entry_xml->getDocumentElement->appendChild( $content );
-
-        foreach my $kw (@{$keywords{$pic->{picid}}}) {
-            my $category = $entry_xml->createElement( 'category' );
-            $category->setAttribute( 'term', $kw );
-            $category->setNamespace( $ns );
-            $entry_xml->getDocumentElement->appendChild( $category );
-        }
-
-        if ( $descriptions{$pic->{picid}} ) {
-            my $content = $entry_xml->createElement( 'title' );
-            $content->setNamespace( $ns );
-            $content->appendTextNode( $descriptions{$pic->{picid}} );
-            $entry_xml->getDocumentElement->appendChild( $content );
-        };
-
-        if($comments{$pic->{picid}}) {
-            my $content = $entry_xml->createElement( "summary" );
-            $content->setNamespace( $ns );
-            $content->appendTextNode( $comments{$pic->{picid}} );
-            $entry_xml->getDocumentElement->appendChild( $content );
-        };
-
-        $feed->add_entry( $entry );
-    }
-
-    return $feed->as_xml;
-}
-
-
-sub create_view_comments {
-    my ( $journalinfo, $u, $opts ) = @_;
-
-    unless ( LJ::is_enabled('latest_comments_rss', $u) ) {
-        $opts->{handler_return} = 404;
-        return 404;
-    }
-
-    unless ( $u->can_use_latest_comments_rss ) {
-        $opts->{handler_return} = 403;
-        return;
-    }
-
-    my $ret = _init_talkview( $journalinfo, $u, $opts, 'comments' );
-
-    my @comments = $u->get_recent_talkitems(25);
-    foreach my $r (@comments)
-    {
-        my $c = LJ::Comment->new($u, jtalkid => $r->{jtalkid});
-        my $thread_url = $c->thread_url;
-        my $subject = $c->subject_raw;
-        LJ::CleanHTML::clean_subject_all(\$subject);
-
-        $ret .= "<item>\n";
-        $ret .= "  <guid isPermaLink='true'>$thread_url</guid>\n";
-        $ret .= "  <pubDate>" . LJ::time_to_http($r->{datepostunix}) . "</pubDate>\n";
-        $ret .= "  <title>" . LJ::exml($subject) . "</title>\n" if $subject;
-        $ret .= "  <link>$thread_url</link>\n";
-        # omit the description tag if we're only syndicating titles
-        unless ($u->{'opt_synlevel'} eq 'title') {
-            my $body = $c->body_raw;
-            LJ::CleanHTML::clean_subject_all(\$body);
-            $ret .= "  <description>" . LJ::exml($body) . "</description>\n";
-        }
-        $ret .= "</item>\n";
-    }
-
-    $ret .= "</channel>\n";
-    $ret .= "</rss>\n";
-
-
-    return $ret;
-}
-
-sub generate_hubbub_jobs {
-    my ( $u, $joblist ) = @_;
-
-    return unless LJ::is_enabled( 'hubbub' );
-
-    foreach my $hub ( @LJ::HUBBUB_HUBS ) {
-        my $make_hubbub_job = sub {
-            my $type = shift;
-
-            my $topic_url = $u->journal_base . "/data/$type";
-            return TheSchwartz::Job->new(
-                funcname => 'TheSchwartz::Worker::PubSubHubbubPublish',
-                arg => {
-                    hub => $hub,
-                    topic_url => $topic_url,
-                },
-                coalesce => $hub,
-            );
-        };
-
-        push @$joblist, $make_hubbub_job->("rss");
-        push @$joblist, $make_hubbub_job->("atom");
-    }
-}
-
-
-1;
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/ljprotocol.pl
--- a/cgi-bin/ljprotocol.pl	Mon Oct 24 19:56:39 2011 +0800
+++ b/cgi-bin/ljprotocol.pl	Tue Oct 25 17:14:09 2011 +0800
@@ -30,10 +30,8 @@
 
 LJ::Config->load;
 
-use lib "$LJ::HOME/cgi-bin";
-
 use LJ::Tags;
-require "ljfeed.pl";
+use LJ::Feed;
 use LJ::EmbedModule;
 
 #### New interface (meta handler) ... other handlers should call into this.
diff -r 9457acb16e2d -r 7181c14fbe5a cgi-bin/modperl_subs.pl
--- a/cgi-bin/modperl_subs.pl	Mon Oct 24 19:56:39 2011 +0800
+++ b/cgi-bin/modperl_subs.pl	Tue Oct 25 17:14:09 2011 +0800
@@ -86,7 +86,7 @@
 use LJ::Support;
 use LJ::CleanHTML;
 use LJ::Talk;
-require "ljfeed.pl";
+use LJ::Feed;
 use LJ::Memories;
 use LJ::Sendmail;
 use LJ::Sysban;
diff -r 9457acb16e2d -r 7181c14fbe5a t/feed-atom.t
--- a/t/feed-atom.t	Mon Oct 24 19:56:39 2011 +0800
+++ b/t/feed-atom.t	Tue Oct 25 17:14:09 2011 +0800
@@ -7,7 +7,7 @@
 require 'ljlib.pl';
 use LJ::Test qw( temp_user );
 
-require 'ljfeed.pl';
+use LJ::Feed;
 use LJ::ParseFeed;
 
 my $u = temp_user();
--------------------------------------------------------------------------------