mourningdove/cgi-bin/LJ/Feed.pm

995 lines
35 KiB
Perl
Raw Permalink Normal View History

2026-05-24 01:03:05 +00:00
#!/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 },
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;
}
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.
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 ( $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 $entry_url = $entry_obj->url;
my $readmore = qq{<b>(<a href="$entry_url">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
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,
{
preformatted => $logprops{$itemid}->{opt_preformatted},
cuturl => $u->{opt_synlevel} eq 'cut' ? $entry_url : "",
to_external_site => 1,
editor => $logprops{$itemid}->{editor},
}
);
# 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 );
}
if ( $u->journaltype eq 'C' && !$opts->{apilinks} ) {
$event =
"Posted by: "
. $posteru{ $it->{posterid} }->ljuser_display
. "<br /><br />"
. $event;
}
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;
}
LJ::EmbedModule->expand_entry( $u, \$event, expand_full => 1 );
}
# include comment count image at bottom of event (for readers
# that don't understand the commentcount)
$event .= "<br /><br />" . $entry_obj->comment_imgtag . " comments"
unless $opts->{'apilinks'} || $r->get_args->{no_comment_count};
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,
tags => [ values %{ $logtags->{$itemid} || {} } ],
security => $it->{security},
posterid => $it->{posterid},
replycount => $logprops{$itemid}->{'replycount'},
url => $entry_url,
};
push @cleanitems, $cleanitem;
push @entries, $entry_obj;
}
# fix up the build date to use entry-time
my $createtime =
$items[0]->{rlogtime}
? $LJ::EndOfTime - $items[0]->{rlogtime}
: $LJ::EndOfTime;
$journalinfo->{builddate} = LJ::time_to_http($createtime);
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 $bot_director = LJ::Hooks::run_hook( "bot_director", "<!-- ", " -->" ) || '';
my $ret;
# header
$ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
$ret .= "$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
### 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";
# use the $ditemid form so it doesn't change
$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>$it->{url}</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>$it->{url}</comments>\n";
}
$ret .= " <category>$_</category>\n" foreach map { LJ::exml($_) } @{ $it->{tags} || [] };
# 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;
my $bot_director = LJ::Hooks::run_hook("bot_director") || '';
if ( $u->should_block_robots ) {
_add_feed_namespace( $feed, "idx", "urn:atom-extension:indexing" );
$xml->getDocumentElement->setAttribute( "idx:index", "no" );
}
$xml->insertBefore( $xml->createComment($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);
}
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 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;
}
# refactored from feeds/index
sub synrow_select {
my %opts = @_; # a single key => val pair
my ( $q, $x ); # what we're looking for
my %optcols = (
url => 's.synurl',
userid => 's.userid',
user => 'u.user',
);
foreach my $k ( keys %optcols ) {
if ( exists $opts{$k} ) {
$x = $opts{$k}; # the data passed in
$q = $optcols{$k}; # the relevant DB column
last;
}
}
die 'LJ::Feed::synrow_select called with invalid arguments' unless $q;
my $dbr = LJ::get_db_reader() or die "No DB";
return $dbr->selectrow_hashref(
"SELECT u.user, s.* FROM syndicated s, useridmap u " . "WHERE u.userid=s.userid AND $q=?",
undef, $x );
}
# code merged in from LJ::Syn module
sub get_popular_feeds {
my $popsyn = LJ::MemCache::get("popsyn");
unless ($popsyn) {
$popsyn = _get_feeds_from_db();
# load u objects so we can get usernames
my %users;
LJ::load_userids_multiple( [ map { $_, \$users{$_} } map { $_->[0] } @$popsyn ] );
unshift @$_, $users{ $_->[0] }->{'user'}, $users{ $_->[0] }->{'name'} foreach @$popsyn;
# format is: [ user, name, userid, synurl, numreaders ]
# set in memcache
my $expire = time() + 3600; # 1 hour
LJ::MemCache::set( "popsyn", $popsyn, $expire );
}
return $popsyn;
}
sub get_popular_feed_ids {
my $popsyn_ids = LJ::MemCache::get("popsyn_ids");
unless ($popsyn_ids) {
my $popsyn = _get_feeds_from_db();
@$popsyn_ids = map { $_->[0] } @$popsyn;
# set in memcache
my $expire = time() + 3600; # 1 hour
LJ::MemCache::set( "popsyn_ids", $popsyn_ids, $expire );
}
return $popsyn_ids;
}
sub _get_feeds_from_db {
my $popsyn = [];
my $dbr = LJ::get_db_reader();
my $sth =
$dbr->prepare( "SELECT userid, synurl, numreaders FROM syndicated "
. "WHERE numreaders > 0 "
. "AND lastnew > DATE_SUB(NOW(), INTERVAL 14 DAY) "
. "ORDER BY numreaders DESC LIMIT 1000" );
$sth->execute();
while ( my @row = $sth->fetchrow_array ) {
push @$popsyn, [@row];
}
return $popsyn;
}
=head2 C<< LJ::Feed::merge( %opts ) >>
=over
=item Opts:
=over
=item from - Merge from: LJ::User or userid
=item from_name - Merge from username
=item to - Merge to LJ::User or userid
=item to_name - Merge to username
=item url - Merge to URL
=item pretend - Do not actually merge
=back
=back
=cut
sub merge_feed {
my %args = @_;
my $from_u;
if ( $args{from_name} ) {
$from_u = LJ::load_user( $args{from_name} )
or return ( 0, "Invalid user: '" . $args{from_name} . "'." );
}
else {
$from_u = LJ::want_user( $args{from} )
or return ( 0, "Invalid from user." );
}
my $to_u;
if ( $args{to_name} ) {
$to_u = LJ::load_user( $args{to_name} )
or return ( 0, "Invalid user: '" . $args{to_name} . "'." );
}
else {
$to_u = LJ::want_user( $args{to} )
or return ( 0, "Invalid to user." );
}
return ( 0, "Trying to merge into yourself." )
if $from_u->equals($to_u);
# we don't want to unlimit this, so reject if we have too many users
my @ids = $from_u->watched_by_userids( limit => $LJ::MAX_WT_EDGES_LOAD + 1 );
return ( 0,
"Unable to merge feeds. Too many users are watching the feed '"
. $from_u->user
. "'. We only allow merges for feeds with at most $LJ::MAX_WT_EDGES_LOAD watchers." )
if scalar @ids > $LJ::MAX_WT_EDGES_LOAD;
foreach ( $to_u, $from_u ) {
return ( 0,
"Invalid user: '"
. $_->user
. "' (statusvis is "
. $_->statusvis
. ", already merged?)" )
unless $_->is_visible;
return ( 0, $_->user . " is not a syndicated account." )
unless $_->is_syndicated;
}
my $url = LJ::CleanHTML::canonical_url( $args{url} )
or return ( 0, "Invalid URL." );
return ( 1, "Everything seems okay" ) if $args{pretend};
my $dbh = LJ::get_db_writer();
my $from_oldurl =
$dbh->selectrow_array( "SELECT synurl FROM syndicated WHERE userid=?", undef, $from_u->id );
my $to_oldurl =
$dbh->selectrow_array( "SELECT synurl FROM syndicated WHERE userid=?", undef, $to_u->id );
# 1) set up redirection for 'from_user' -> 'to_user'
$from_u->update_self( { journaltype => 'R', statusvis => 'R' } );
$from_u->set_prop( "renamedto", $to_u->user )
or return ( 0, "Unable to set userprop. Database unavailable?" );
# 2) delete the row in the syndicated table for the user
# that is now renamed
$dbh->do( "DELETE FROM syndicated WHERE userid=?", undef, $from_u->id );
return ( 0, "Database Error: " . $dbh->errstr )
if $dbh->err;
# 3) update the url of the destination syndicated account and
# tell it to check it now
$dbh->do( "UPDATE syndicated SET synurl=?, checknext=NOW(), failcount=0 WHERE userid=?",
undef, $url, $to_u->id );
return ( 0, "Database Error: " . $dbh->errstr )
if $dbh->err;
# 4) make users who watch 'from_user' now watch 'to_user'
# we can't just use delete_ and add_ edges, because we would lose
# custom group and colors data
if (@ids) {
# update ignore so we don't raise duplicate key errors
$dbh->do( 'UPDATE IGNORE wt_edges SET to_userid=? WHERE to_userid=?',
undef, $to_u->id, $from_u->id );
return ( 0, "Database Error: " . $dbh->errstr )
if $dbh->err;
# in the event that some rows in the update above caused a duplicate key error,
# we can delete the rows that weren't updated, since they don't need to be
# processed anyway
$dbh->do( "DELETE FROM wt_edges WHERE to_userid=?", undef, $from_u->id );
return ( 0, "Database Error: " . $dbh->errstr )
if $dbh->err;
# clear memcache keys
foreach my $id (@ids) {
LJ::memcache_kill( $id, 'wt_edges' );
LJ::memcache_kill( $id, 'wt_list' );
LJ::memcache_kill( $id, 'watched' );
}
LJ::memcache_kill( $from_u->id, 'wt_edges_rev' );
LJ::memcache_kill( $from_u->id, 'watched_by' );
LJ::memcache_kill( $to_u->id, 'wt_edges_rev' );
LJ::memcache_kill( $to_u->id, 'watched_by' );
}
# log to statushistory
my $remote = LJ::get_remote();
my $msg = "Merged " . $from_u->user . " to " . $to_u->user . " using URL: $url.";
LJ::statushistory_add( $from_u, $remote, 'synd_merge', $msg . " Old URL was $from_oldurl." );
LJ::statushistory_add( $to_u, $remote, 'synd_merge', $msg . " Old URL was $to_oldurl." );
return ( 1, $msg );
}
1;