#!/usr/bin/perl use strict; package LJ::Feed; my %feedtypes = ( rss => \&create_view_rss, atom => \&create_view_atom, foaf => \&create_view_foaf, ); sub make_feed { my ($r, $u, $remote, $opts) = @_; $opts->{pathextra} =~ s!^/(\w+)!!; my $feedtype = $1; my $viewfunc = $feedtypes{$feedtype}; unless ($viewfunc) { $opts->{'handler_return'} = 404; return undef; } $opts->{noitems} = 1 if $feedtype eq 'foaf'; $r->notes('codepath' => "feed.$feedtype") if $r; my $dbr = LJ::get_db_reader(); my $user = $u->{'user'}; if ($u->{'journaltype'} eq "R" && $u->{'renamedto'} ne "") { $opts->{'redir'} = LJ::journal_base($u->{'renamedto'}, $opts->{'vhost'}) . "/data/$feedtype"; return undef; } LJ::load_user_props($u, qw/ journaltitle journalsubtitle opt_synlevel /); LJ::text_out(\$u->{$_}) foreach ("name", "url", "urlname"); # opt_synlevel will default to 'full' $u->{'opt_synlevel'} = 'full' unless $u->{'opt_synlevel'} =~ /^(?:full|summary|title)$/; # some data used throughout the channel my $journalinfo = { u => $u, link => LJ::journal_base($u) . "/", title => $u->{journaltitle} || $u->{name} || $u->{user}, subtitle => $u->{journalsubtitle} || $u->{name}, builddate => LJ::time_to_http(time()), }; # if we do not want items for this view, just call out return $viewfunc->($journalinfo, $u, $opts) if ($opts->{'noitems'}); # 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->{'journaltype'} eq 'Y') { 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; } ## load the itemids my @itemids; my @items = LJ::get_recent_items({ 'clusterid' => $u->{'clusterid'}, 'clustersource' => 'slave', 'remote' => $remote, 'userid' => $u->{'userid'}, 'itemshow' => 25, 'order' => "logtime", '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}; $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. if ((my $status = $r->meets_conditions) != Apache::Constants::OK()) { $opts->{handler_return} = $status; return undef; } # email address of journal owner, but respect their privacy settings if ($u->{'allow_contactshow'} eq "Y" && $u->{'opt_whatemailshow'} ne "N" && $u->{'opt_mangleemail'} ne "Y") { my $cemail; # default to their actual email $cemail = $u->{'email'}; # use their livejournal email if they have one if ($LJ::USER_EMAIL && $u->{'opt_whatemailshow'} eq "L" && LJ::get_cap($u, "useremail") && ! $u->{'no_mail_alias'}) { $cemail = "$u->{'user'}\@$LJ::USER_DOMAIN"; } # clean it up since we know we have one now $journalinfo->{email} = $cemail; } # 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; ENTRY: foreach my $it (@items) { # load required data my $itemid = $it->{'itemid'}; my $ditemid = $itemid*256 + $it->{'anum'}; next ENTRY if $posteru{$it->{'posterid'}} && $posteru{$it->{'posterid'}}->{'statusvis'} eq 'S'; 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 = "({link}$ditemid.html\">Read more ...)"; # 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 (LJ::get_cap($u, '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'} }); # 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') { # assume the first paragraph is terminated by two
or a

# valid XML tags should be handled, even though it makes an uglier regex if ($event =~ m!((()?\s*){2})|()!i) { # everything before the matched tag + the tag itself # + a link to read more $event = $` . $& . $readmore; } } if ($event =~ //) { my $pollid = $1; my $name = $dbr->selectrow_array("SELECT name FROM poll WHERE pollid=?", undef, $pollid); if ($name) { LJ::Poll::clean_poll(\$name); } else { $name = "#$pollid"; } $event =~ s!!
View Poll: $name
!g; } $ppid = $1 if $event =~ m!!; } my $mood; if ($logprops{$itemid}->{'current_mood'}) { $mood = $logprops{$itemid}->{'current_mood'}; } elsif ($logprops{$itemid}->{'current_moodid'}) { $mood = LJ::mood_name($logprops{$itemid}->{'current_moodid'}+0); } my $createtime = $LJ::EndOfTime - $it->{rlogtime}; 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 => ($logprops{$itemid}->{'opt_nocomments'} == 0), music => $logprops{$itemid}->{'current_music'}, mood => $mood, ppid => $ppid, tags => [ values %{$logtags->{$itemid} || {}} ], }; push @cleanitems, $cleanitem; } # fix up the build date to use entry-time $journalinfo->{'builddate'} = LJ::time_to_http($LJ::EndOfTime - $items[0]->{'rlogtime'}), return $viewfunc->($journalinfo, $u, $opts, \@cleanitems); } # the creator for the RSS XML syndication view sub create_view_rss { my ($journalinfo, $u, $opts, $cleanitems) = @_; my $ret; # header $ret .= "{'saycharset'}' ?>\n"; $ret .= LJ::run_hook("bot_director", "") . "\n"; $ret .= "\n"; # channel attributes $ret .= "\n"; $ret .= " " . LJ::exml($journalinfo->{title}) . "\n"; $ret .= " $journalinfo->{link}\n"; $ret .= " " . LJ::exml("$journalinfo->{title} - $LJ::SITENAME") . "\n"; $ret .= " " . LJ::exml($journalinfo->{email}) . "\n" if $journalinfo->{email}; $ret .= " $journalinfo->{builddate}\n"; $ret .= " LiveJournal / $LJ::SITENAME\n"; # TODO: add 'language' field when user.lang has more useful information ### image block, returns info for their current userpic if ($u->{'defaultpicid'}) { my $pic = {}; LJ::load_userpics($pic, [ $u, $u->{'defaultpicid'} ]); $pic = $pic->{$u->{'defaultpicid'}}; # flatten $ret .= " \n"; $ret .= " $LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}\n"; $ret .= " " . LJ::exml($journalinfo->{title}) . "\n"; $ret .= " $journalinfo->{link}\n"; $ret .= " $pic->{'width'}\n"; $ret .= " $pic->{'height'}\n"; $ret .= " \n\n"; } 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}; $ret .= "\n"; $ret .= " $journalinfo->{link}$ditemid.html\n"; $ret .= " " . LJ::time_to_http($it->{createtime}) . "\n"; $ret .= " " . LJ::exml($it->{subject}) . "\n" if $it->{subject}; $ret .= " " . LJ::exml($journalinfo->{email}) . "" if $journalinfo->{email}; $ret .= " $journalinfo->{link}$ditemid.html\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 .= " " . LJ::exml($it->{event}) . "\n"; } if ($it->{comments}) { $ret .= " $journalinfo->{link}$ditemid.html\n"; } $ret .= " $_\n" foreach map { LJ::exml($_) } @{$it->{tags} || []}; # support 'podcasting' enclosures $ret .= LJ::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::exml($it->{music}) . "\n" if $it->{music}; $ret .= " " . LJ::exml($it->{mood}) . "\n" if $it->{mood}; $ret .= "\n"; } $ret .= "\n"; $ret .= "\n"; return $ret; } # the creator for the Atom view # keys of $opts: # saycharset - required: the charset of the feed # noheader - only output an .. block. off by default # apilinks - output AtomAPI links for posting a new entry or # getting/editing/deleting an existing one. off by default # TODO: define and use an 'lj:' namespace sub create_view_atom { my ($journalinfo, $u, $opts, $cleanitems) = @_; my $ret; # prolog line $ret .= "{'saycharset'}' ?>\n"; $ret .= LJ::run_hook("bot_director", ""); # AtomAPI interface my $api = $opts->{'apilinks'} ? "$LJ::SITEROOT/interface/atom" : "$LJ::SITEROOT/users/$u->{user}/data/atom"; # header unless ($opts->{'noheader'}) { $ret .= "\n"; # attributes $ret .= "" . LJ::exml($journalinfo->{title}) . "\n"; $ret .= "" . LJ::exml($journalinfo->{subtitle}) . "\n" if $journalinfo->{subtitle}; $ret .= "\n"; # last update $ret .= "" . LJ::time_to_w3c($journalinfo->{'modtime'}, 'Z') . ""; # link to the AtomAPI version of this feed $ret .= "" : "' href='$api' />"; if ($opts->{'apilinks'}) { $ret .= ""; } } # output individual item blocks foreach my $it (@$cleanitems) { my $itemid = $it->{itemid}; my $ditemid = $it->{ditemid}; $ret .= " \n"; # include empty tag if we don't have a subject. $ret .= " " . LJ::exml($it->{subject}) . "\n"; $ret .= " urn:lj:$LJ::DOMAIN:atom1:$journalinfo->{u}{user}:$ditemid\n"; $ret .= " \n"; if ($opts->{'apilinks'}) { $ret .= ""; } $ret .= " " . LJ::time_to_w3c($it->{createtime}, 'Z') . "\n" if $it->{createtime} != $it->{modtime}; my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime}); $ret .= " " . sprintf("%04d-%02d-%02dT%02d:%02d:%02d", $year, $mon, $mday, $hour, $min, $sec) . "\n"; $ret .= " " . LJ::time_to_w3c($it->{modtime}, 'Z') . "\n"; $ret .= " \n"; $ret .= " " . LJ::exml($journalinfo->{u}{name}) . "\n"; $ret .= " " . LJ::exml($journalinfo->{email}) . "\n" if $journalinfo->{email}; $ret .= " \n"; $ret .= " \n" foreach map { LJ::exml($_) } @{$it->{tags} || []}; # 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 if ($u->{'opt_synlevel'} eq 'full') { $ret .= " " . LJ::exml($it->{event}) . "\n"; } elsif ($u->{'opt_synlevel'} eq 'summary') { $ret .= " " . LJ::exml($it->{event}) . "\n"; } $ret .= " \n"; } unless ($opts->{'noheader'}) { $ret .= "\n"; } return $ret; } # create a FOAF page for a user sub create_view_foaf { my ($journalinfo, $u, $opts) = @_; my $comm = ($u->{journaltype} eq 'C'); my $ret; # return nothing if we're not a user unless ($u->{journaltype} eq 'P' || $comm) { $opts->{handler_return} = 404; return undef; } # set our content type $opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset}; # setup userprops we will need LJ::load_user_props($u, qw{ aolim icq yahoo jabber msn url urlname external_foaf_url }); # create bare foaf document, for now $ret = "\n"; $ret .= LJ::run_hook("bot_director", ""); $ret .= "\n"; # precompute some values my $digest = Digest::SHA1::sha1_hex('mailto:' . $u->{email}); # channel attributes $ret .= ($comm ? " \n" : " \n"); $ret .= " $u->{user}\n"; if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->{allow_infoshow} eq 'Y') { my $bdate = $u->{bdate}; $bdate =~ s/^0000-//; $ret .= " $bdate\n"; } $ret .= " $digest\n"; $ret .= " \n"; $ret .= " {user}\">\n"; $ret .= " $LJ::SITENAME Profile\n"; $ret .= " Full $LJ::SITENAME profile, including information such as interests and bio.\n"; $ret .= " \n"; $ret .= " \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 .= " {external_foaf_url}) . "\" />\n"; $ret .= ($comm ? " \n" : " \n"); $ret .= "\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 $u->{$type}; $ret .= " " . LJ::exml($u->{$type}) . "\n"; } } # include a user's journal page and web site info $ret .= " \n"; if ($u->{url}) { $ret .= " {url}); $ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n"; } # interests, please! # arrayref of interests rows: [ intid, intname, intcount ] my $intu = LJ::get_interests($u); foreach my $int (@$intu) { LJ::text_out(\$int->[1]); # 1==interest $ret .= " [1]) . "\" " . "rdf:resource=\"$LJ::SITEROOT/interests.bml?int=" . LJ::eurl($int->[1]) . "\" />\n"; } # check if the user has a "FOAF-knows" group my $groups = LJ::get_friend_group($u->{userid}, { name => 'FOAF-knows' }); my $mask = $groups ? 1 << $groups->{groupnum} : 0; # now information on who you know, limited to a certain maximum number of users my $friends = LJ::get_friends($u->{userid}, $mask); my @ids = keys %$friends; @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS; # now load my %users; LJ::load_userids_multiple([ map { $_, \$users{$_} } @ids ], [$u]); # iterate to create data structure foreach my $friendid (@ids) { next if $friendid == $u->{userid}; my $fu = $users{$friendid}; next if $fu->{statusvis} =~ /[DXS]/ || $fu->{journaltype} ne 'P'; $ret .= $comm ? " \n" : " \n"; $ret .= " \n"; $ret .= " $fu->{'user'}\n"; $ret .= " \n"; $ret .= " \n"; $ret .= " \n"; $ret .= $comm ? " \n" : " \n"; } # finish off the document $ret .= $comm ? " \n" : " \n"; $ret .= "\n"; return $ret; } 1;