994 lines
35 KiB
Perl
994 lines
35 KiB
Perl
#!/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;
|