LJSUP-17669: Login.bml form refactoring
[livejournal.git] / cgi-bin / ljfeed.pl
blob47752a8f4be2f7a586476cdfe702d82e23a3b35a
1 #!/usr/bin/perl
3 package LJ::Feed;
4 use strict;
5 no warnings 'uninitialized';
7 use LJ::Entry;
8 use LJ::Entry::Repost;
9 use LJ::Request;
10 use LJ::TimeUtil;
11 use XML::Atom::Person;
12 use XML::Atom::Feed;
14 my %feedtypes = (
15 'rss' => { handler => \&create_view_rss, need_items => 1 },
16 'atom' => { handler => \&create_view_atom, need_items => 1 },
17 'foaf' => { handler => \&create_view_foaf, },
18 'yadis' => { handler => \&create_view_yadis, },
19 'userpics' => { handler => \&create_view_userpics, },
20 'comments' => { handler => \&create_view_comments, },
21 'rss_friends' => { handler => \&create_view_rss, need_items => 1, paid_only => 1 },
22 'atom_friends' => { handler => \&create_view_atom, need_items => 1, paid_only => 1 },
25 sub make_feed {
26 my ($u, $remote, $opts) = @_;
28 $opts->{pathextra} =~ s!^/(\w+)!!;
29 my $feedtype = $1;
30 my $viewfunc = $feedtypes{$feedtype};
32 # /ya/rss -> special rss for yandex
33 if ($feedtype eq 'ya') {
34 $opts->{pathextra} =~ s!^/(\w+)!!; # cut off '/rss' part
35 $feedtype = $1;
36 $viewfunc = $feedtypes{$feedtype} if $feedtype eq 'rss';
37 $opts->{include_statistics} = 1;
39 my $allowed = 0;
40 my $remote_ip = LJ::get_remote_ip();
42 foreach my $block ( @LJ::YANDEX_RSS_IP_BLOCKS ) {
43 my $net = Net::Netmask->new($block);
45 next unless $net->match($remote_ip);
46 $allowed = 1;
47 last;
50 unless ( $allowed ) {
51 $opts->{'handler_return'} = 403;
52 return undef;
56 unless ( $viewfunc ) {
57 $opts->{'handler_return'} = 404;
58 return undef;
61 LJ::Request->notes('codepath' => "feed.$feedtype") if LJ::Request->is_inited;
63 my $dbr = LJ::get_db_reader();
65 my $user = $u->{'user'};
67 LJ::load_user_props($u, qw/journaltitle journalsubtitle opt_synlevel/);
69 LJ::text_out(\$u->{$_})
70 foreach qw/name url urlname/;
72 # opt_synlevel will default to 'full'
73 $u->{'opt_synlevel'} = 'full'
74 unless $u->{'opt_synlevel'} =~ /^(?:full|summary|title)$/;
76 # some data used throughout the channel
77 my $journalinfo = {
78 u => $u,
79 link => LJ::journal_base($u) . "/",
80 title => $u->{journaltitle} || $u->{name} || $u->{user},
81 subtitle => $u->{journalsubtitle} || $u->{name},
82 builddate => LJ::TimeUtil->time_to_http(time()),
85 # if we do not want items for this view, just call out
86 $opts->{'contenttype'} = 'text/xml; charset='.$opts->{'saycharset'};
88 LJ::run_hooks('make_feed', $feedtype, $u, { remote => $remote });
90 return $viewfunc->{handler}->($journalinfo, $u, $opts)
91 unless $viewfunc->{need_items};
93 # for syndicated accounts, redirect to the syndication URL
94 # However, we only want to do this if the data we're returning
95 # is similar. (Not FOAF, for example)
96 if ( $u->{'journaltype'} eq 'Y' ) {
97 my $synurl = $dbr->selectrow_array("SELECT synurl FROM syndicated WHERE userid=$u->{'userid'}");
98 return 'No syndication URL available.' unless $synurl;
100 $opts->{'redir'} = $synurl;
101 return undef;
104 my %FORM = LJ::Request->args;
106 ## load the itemids
107 my (@itemids, @objs);
109 # for consistency, we call ditemids "itemid" in user-facing settings
110 my $ditemid = $FORM{itemid} + 0;
112 if ($ditemid) {
113 my $entry = LJ::Entry->new($u, ditemid => $ditemid);
115 if ( ! $entry || ! $entry->valid || ! $entry->visible_to($remote) ) {
116 $opts->{'handler_return'} = 404;
117 return undef;
120 if (LJ::Entry::Repost->substitute_content($entry,
121 { 'original_post_obj' => \$entry,} )) {
123 if ( ! $entry || ! $entry->valid || ! $entry->visible_to($remote) ) {
124 $opts->{'handler_return'} = 404;
125 return undef;
129 push @objs, $entry;
131 elsif ($viewfunc->{'paid_only'} && $u->get_cap('paid')) {
132 @objs = map { $_->{'entry'} } @{ LJ::Journal::FriendsFeed->get_items(
133 'remoteid' => $remote ? $remote->userid : 0,
134 'userid' => $u->{'userid'},
135 'itemshow' => 25,
136 ) };
138 # Warning: array @itemids is not filling here, so entries will be outputted without tags.
140 $journalinfo->{title} .= ' ' . LJ::Lang::ml('feeds.title.friends');
141 $journalinfo->{link} .= 'friends/';
143 else {
144 LJ::get_recent_items({
145 'remote' => $remote,
146 'userid' => $u->{'userid'},
147 'itemshow' => 25,
148 'order' => 'logtime',
149 'tagids' => $opts->{tagids},
150 'tagmode' => $opts->{tagmode},
151 'itemids' => \@itemids,
152 'friendsview' => 1, # this returns rlogtimes
153 'dateformat' => 'S2', # S2 format time format is easier
154 'entry_objects' => \@objs,
155 'load_props' => 1,
156 'load_text' => 1,
160 $opts->{'contenttype'} = 'text/xml; charset=' . $opts->{'saycharset'};
162 # set last-modified header, then let apache figure out
163 # whether we actually need to send the feed.
164 my $lastmod = 0;
166 for my $obj (@objs) {
167 # revtime of the item.
168 my $revtime = $obj->prop('revtime');
169 $lastmod = $revtime if $revtime > $lastmod;
171 unless ($revtime) {
172 # use the logtime of the item.
173 my $itime = $LJ::EndOfTime - $obj->{rlogtime};
174 $lastmod = $itime if $itime > $lastmod;
178 LJ::Request->set_last_modified($lastmod) if $lastmod;
180 # use this $lastmod as the feed's last-modified time
181 # we would've liked to use something like
182 # LJ::get_timeupdate_multi instead, but that only changes
183 # with new updates and doesn't change on edits.
184 $journalinfo->{'modtime'} = $lastmod;
186 # regarding $r->set_etag:
187 # http://perl.apache.org/docs/general/correct_headers/correct_headers.html#Entity_Tags
188 # It is strongly recommended that you do not use this method unless you
189 # know what you are doing. set_etag() is expecting to be used in
190 # conjunction with a static request for a file on disk that has been
191 # stat()ed in the course of the current request. It is inappropriate and
192 # "dangerous" to use it for dynamic content.
193 if ((my $status = LJ::Request->meets_conditions) != LJ::Request::OK()) {
194 $opts->{handler_return} = $status;
195 return undef;
198 $journalinfo->{email} = $u->email_for_feeds if $u && $u->email_for_feeds;
200 # load tags now that we have no chance of jumping out early
201 my $logtags = LJ::Tags::get_logtags($u, \@itemids);
203 my @cleanitems;
205 ENTRY:
206 foreach my $entry_obj (@objs) {
207 next ENTRY if $entry_obj->poster->{'statusvis'} eq 'S';
208 next ENTRY if $entry_obj && $entry_obj->is_suspended_for($remote);
210 next ENTRY if $entry_obj->original_post;
212 my $ditemid = $entry_obj->{ditemid};
213 if ( $LJ::UNICODE && $entry_obj->prop('unknown8bit') ) {
214 LJ::item_toutf8(
216 \$entry_obj->{'subject'},
217 \$entry_obj->{'event'},
218 $entry_obj->prop,
222 # see if we have a subject and clean it
223 my $subject = $entry_obj->{'subject'};
225 if ($subject) {
226 $subject =~ s/[\r\n]/ /g;
227 LJ::CleanHTML::clean_subject_all(\$subject);
230 # an HTML link to the entry. used if we truncate or summarize
231 my $readmore = "<b>(<a href=\"$journalinfo->{link}$ditemid.html\">Read more ...</a>)</b>";
233 # empty string so we don't waste time cleaning an entry that won't be used
234 my $event = $u->{'opt_synlevel'} eq 'title' ? '' : $entry_obj->event_raw;
236 # clean the event, if non-empty
237 my $ppid = 0;
239 if ( $event ) {
240 # users without 'full_rss' get their logtext bodies truncated
241 # do this now so that the html cleaner will hopefully fix html we break
242 unless (LJ::get_cap($u, 'full_rss')) {
243 my $trunc = LJ::text_trim($event, 0, 80);
244 $event = "$trunc $readmore" if $trunc ne $event;
247 LJ::CleanHTML::clean_event(\$event,
249 'wordlength' => 0,
250 'preformatted' => $entry_obj->prop('opt_preformatted'),
251 'journalid' => $u->userid,
252 'posterid' => $entry_obj->{'posterid'},
253 'entry_url' => $entry_obj->url,
254 'textonly' => 1,
258 # do this after clean so we don't have to about know whether or not
259 # the event is preformatted
260 if ( $u->{'opt_synlevel'} eq 'summary' ) {
261 # assume the first paragraph is terminated by two <br> or a </p>
262 # valid XML tags should be handled, even though it makes an uglier regex
263 if ( $event =~ m!
264 (.*?) ## any text
265 (?=<) ## followed by "<" (zero-width positive look-ahead assertion)
266 ## and then either </p> or 2 BRs,
267 ## where BR is one of: <br></br>, <br> or <br/>
268 ( (?:<br\s*/?\>(?:</br\s*>)?\s*){2} | (?:</p\s*>) ) !six )
270 # everything before the matched tag + the tag itself
271 # + a link to read more
272 $event = $1 . $2 . $readmore;
276 while ( $event =~ /<lj-poll-(\d+)>/g ) {
277 my $pollid = $1;
279 my $name = LJ::Poll->new($pollid)->name;
280 if ( $name ) {
281 LJ::Poll->clean_poll(\$name);
283 else {
284 $name = "#$pollid";
287 $event =~ s!<lj-poll-$pollid>!<div><a href="$LJ::SITEROOT/poll/?id=$pollid">View Poll: $name</a></div>!g;
290 my %args = LJ::Request->args;
291 LJ::EmbedModule->expand_entry($u, \$event, expand_full => 1)
292 if %args && $args{'unfold_embed'};
294 $ppid = $1
295 if $event =~ m!<lj-phonepost journalid=[\'\"]\d+[\'\"] dpid=[\'\"](\d+)[\'\"]( /)?>!;
298 my $mood;
300 if ( $entry_obj->prop('current_mood') ) {
301 $mood = $entry_obj->prop('current_mood');
303 elsif ( $entry_obj->prop('current_moodid') ) {
304 $mood = LJ::mood_name($entry_obj->prop('current_moodid') + 0);
307 my $alldateparts = $entry_obj->{'eventtime'};
308 $alldateparts =~ s/[-:]/ /g;
310 my $createtime = $LJ::EndOfTime - $entry_obj->{rlogtime};
311 push @cleanitems, {
312 entry => $entry_obj,
313 itemid => $entry_obj->jitemid,
314 ditemid => $ditemid,
315 subject => $subject,
316 event => $event,
317 createtime => $createtime,
318 eventtime => $alldateparts,
319 modtime => $entry_obj->prop('revtime') || $createtime,
320 comments => $entry_obj->comments_shown,
321 music => $entry_obj->prop('current_music'),
322 mood => $mood,
323 ppid => $ppid,
324 tags => [ values %{$logtags->{$entry_obj->jitemid} || {}} ],
325 security => $entry_obj->security,
326 posterid => $entry_obj->poster->id,
327 replycount => $entry_obj->prop('replycount'),
328 posteruser => $entry_obj->poster->user,
332 # fix up the build date to use entry-time
333 $journalinfo->{'builddate'} = LJ::TimeUtil->time_to_http($LJ::EndOfTime - $objs[0]->{'rlogtime'}),
335 return $viewfunc->{handler}->($journalinfo, $u, $opts, \@cleanitems, \@objs);
338 # the creator for the RSS XML syndication view
339 sub create_view_rss {
340 my ($journalinfo, $u, $opts, $cleanitems, $objs) = @_;
341 my $ret;
343 # For Yandex ( http://blogs.yandex.ru/faq.xml?id=542563 )
344 # if 'copyright' tag contains 'noindex', this rss will not be indexed.
345 my $copyright = $u->should_block_robots ? 'NOINDEX' : '';
347 # header
348 $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
349 $ret .= LJ::run_hook("bot_director", "<!-- ", " -->") . "\n";
350 $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/' " .
351 "xmlns:media='http://search.yahoo.com/mrss/' " .
352 "xmlns:atom10='http://www.w3.org/2005/Atom'>\n";
354 # channel attributes
355 $ret .= "<channel>\n";
356 $ret .= " <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
357 $ret .= " <link>$journalinfo->{link}</link>\n";
358 $ret .= " <description>" . LJ::exml("$journalinfo->{title} - $LJ::SITENAME") . "</description>\n";
359 $ret .= " <managingEditor>" . LJ::exml($journalinfo->{email}) . "</managingEditor>\n" if $journalinfo->{email};
360 $ret .= " <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
361 $ret .= " <generator>LiveJournal / $LJ::SITENAME</generator>\n";
362 $ret .= " <lj:journal>" . $u->user . "</lj:journal>\n";
363 $ret .= " <lj:journalid>" . $u->userid . "</lj:journalid>\n";
364 $ret .= " <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
365 $ret .= " <copyright>" . $copyright . "</copyright>\n" if $copyright;
366 # TODO: add 'language' field when user.lang has more useful information
368 unless ($LJ::DISABLED{'hubbub_discovery'}) {
369 foreach my $hub (@LJ::HUBBUB_HUBS) {
370 $ret .= " <atom10:link rel='hub' href='" . LJ::exml($hub) . "' />\n";
374 ### image block, returns info for their current userpic
375 if ($u->{'defaultpicid'}) {
376 my $pic = {};
377 LJ::load_userpics($pic, [ $u, $u->{'defaultpicid'} ]);
378 $pic = $pic->{$u->{'defaultpicid'}}; # flatten
380 $ret .= " <image>\n";
381 $ret .= " <url>$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}</url>\n";
382 $ret .= " <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
383 $ret .= " <link>$journalinfo->{link}</link>\n";
384 $ret .= " <width>$pic->{'width'}</width>\n";
385 $ret .= " <height>$pic->{'height'}</height>\n";
386 $ret .= " </image>\n\n";
389 # output individual item blocks
390 foreach my $it (@$cleanitems) {
391 my $entry = $it->{entry};
392 my $itemid = $it->{itemid};
393 my $ditemid = $it->{ditemid};
394 my $url = $entry->url;
396 $ret .= "<item>\n";
397 $ret .= " <guid isPermaLink='true'>$url</guid>\n";
398 $ret .= " <pubDate>" . LJ::TimeUtil->time_to_http($it->{createtime}) . "</pubDate>\n";
399 $ret .= " <title>" . LJ::exml($it->{subject}) . "</title>\n" if $it->{subject};
400 $ret .= " <author>" . LJ::exml($journalinfo->{email}) . "</author>" if $journalinfo->{email};
401 $ret .= " <link>$url</link>\n";
402 # omit the description tag if we're only syndicating titles
403 # note: the $event was also emptied earlier, in make_feed
404 unless ($u->{'opt_synlevel'} eq 'title') {
405 $ret .= " <description>" . LJ::exml($it->{event}) . "</description>\n";
407 if ($it->{comments}) {
408 $ret .= " <comments>" . $entry->url . "</comments>\n";
410 $ret .= " <category>$_</category>\n" foreach map { LJ::exml($_) } @{$it->{tags} || []};
411 # support 'podcasting' enclosures
412 $ret .= LJ::run_hook( "pp_rss_enclosure",
413 { userid => $u->{userid}, ppid => $it->{ppid} }) if $it->{ppid};
414 # TODO: add author field with posterid's email address, respect communities
415 $ret .= " <lj:music>" . LJ::exml($it->{music}) . "</lj:music>\n" if $it->{music};
416 $ret .= " <media:title type=\"plain\">" . LJ::exml($it->{music}) . "</media:title>\n" if $it->{music};
417 $ret .= " <lj:mood>" . LJ::exml($it->{mood}) . "</lj:mood>\n" if $it->{mood};
418 $ret .= " <lj:security>" . LJ::exml($it->{security}) . "</lj:security>\n" if $it->{security};
419 unless ($u->{'userid'} == $it->{'posterid'}) {
420 $ret .= " <lj:poster>" . LJ::exml($it->{'posteruser'}) . "</lj:poster>\n";
421 $ret .= " <lj:posterid>" . $it->{'posterid'} . "</lj:posterid>\n";
423 $ret .= " <lj:reply-count>$it->{replycount}</lj:reply-count>\n";
425 if ($opts->{include_statistics}) {
427 my $now = DateTime->now(time_zone => 'Europe/Moscow');
428 my $yesterday = $now->clone->subtract(days => 1);
430 # visitors
431 my $data_v = LJ::PersonalStats::DB->fetch('post_stats', {
432 type => 0, # post views only in journal
433 date => $now->strftime("%Y-%m"),
434 journal_id => $u->userid,
435 post_id => $ditemid,
437 # hits, today and yesterday
438 my $data_t = LJ::PersonalStats::DB->fetch('post_stats', {
439 type => 0, # post views only in journal
440 date => $now->strftime("%Y-%m-%d"),
441 journal_id => $u->userid,
442 post_id => $ditemid,
444 my $data_y = LJ::PersonalStats::DB->fetch('post_stats', {
445 type => 0, # post views only in journal
446 date => $yesterday->strftime("%Y-%m-%d"),
447 journal_id => $u->userid,
448 post_id => $ditemid,
451 # sum last 24 hours: 00 to current hour and current hour + 1 to 23 in yesterday
452 my $sum = 0;
454 my @today_hits;
455 foreach my $el (@$data_t) {
456 $today_hits[ $el->{time_id} ] = $el->{hits};
459 for (my $i = 0; $i <= $now->hour; $i++) {
460 $sum += $today_hits[$i];
463 if ($now->hour < 23) {
464 my @yesterday_hits;
465 foreach my $el (@$data_y) {
466 $yesterday_hits[ $el->{time_id} ] = $el->{hits};
469 for (my $i = $now->hour + 1; $i <= 23; $i++) {
470 $sum += $yesterday_hits[$i];
474 $ret .= "<pageviews>$sum</pageviews>\n";
476 my @day_visitors;
477 foreach my $el (@$data_v) {
478 $day_visitors[ $el->{time_id} ] = $el->{visitors};
480 my $today_visitors = $day_visitors[$now->day] || 0;
482 $ret .= "<visitors>$today_visitors</visitors>\n";
485 $ret .= "</item>\n";
488 $ret .= "</channel>\n";
489 $ret .= "</rss>\n";
491 return $ret;
495 # the creator for the Atom view
496 # keys of $opts:
497 # single_entry - only output an <entry>..</entry> block. off by default
498 # apilinks - output AtomAPI links for posting a new entry or
499 # getting/editing/deleting an existing one. off by default
500 # TODO: define and use an 'lj:' namespace
502 # TODO: Remove lines marked with 'COMPAT' - they are only present
503 # to allow backwards compatibility with atom parsers that are pre 0.6-draft.
504 # We create tags valid for 1.1-draft, but we want to be nice during
505 # atom's (and atom users) continuing transition. 1.0 parsers, according
506 # to spec, should NOT barf on unknown tags.
507 # * Where we can't be compatible, we use Atom 1.0. *
508 # http://www.ietf.org/internet-drafts/draft-ietf-atompub-format-11.txt
510 sub create_view_atom
512 my ( $j, $u, $opts, $cleanitems, $entrylist ) = @_;
513 my ( $feed, $xml, $ns );
515 $ns = "http://www.w3.org/2005/Atom";
517 # Strip namespace from child tags. Set default namespace, let
518 # child tags inherit from it. So ghetto that we even have to do this
519 # and LibXML can't on its own.
520 my $normalize_ns = sub {
521 my $str = shift;
522 $str =~ s/(<\w+)\s+xmlns="\Q$ns\E"/$1/og;
523 $str =~ s/<feed\b/<feed xmlns="$ns" xmlns:lj="$LJ::SITEROOT"/;
524 $str =~ s/<entry>/<entry xmlns="$ns" xmlns:lj="$LJ::SITEROOT">/ if $opts->{'single_entry'};
525 return $str;
528 # AtomAPI interface path
529 my $api = $opts->{'apilinks'} ? "$LJ::SITEROOT/interface/atom" :
530 $u->journal_base . "/data/atom";
532 my $make_link = sub {
533 my ( $rel, $type, $href, $title ) = @_;
534 my $link = XML::Atom::Link->new( Version => 1 );
535 $link->rel($rel);
536 $link->type($type) if $type;
537 $link->href($href);
538 $link->title( $title ) if $title;
539 return $link;
542 my $author = XML::Atom::Person->new( Version => 1 );
543 my $journalu = $j->{u};
544 $author->email( $journalu->email_for_feeds ) if $journalu && $journalu->email_for_feeds;
545 $author->name( $u->{'name'} );
547 # feed information
548 unless ($opts->{'single_entry'}) {
549 $feed = XML::Atom::Feed->new( Version => 1 );
550 $xml = $feed->{doc};
552 unless ($xml){
553 die "Error: XML-LibXML is required"; ## sudo yum install perl-XML-LibXML
556 if ($u->should_block_robots) {
557 $xml->getDocumentElement->setAttribute( "xmlns:idx", "urn:atom-extension:indexing" );
558 $xml->getDocumentElement->setAttribute( "idx:index", "no" );
561 $xml->insertBefore( $xml->createComment( LJ::run_hook("bot_director") ), $xml->documentElement());
563 # attributes
564 $feed->id( "urn:lj:$LJ::DOMAIN:atom1:$u->{user}" );
565 $feed->title( $j->{'title'} || $u->{user} );
566 if ( $j->{'subtitle'} ) {
567 $feed->subtitle( $j->{'subtitle'} );
570 $feed->author( $author );
571 $feed->add_link( $make_link->( 'alternate', 'text/html', $j->{'link'} ) );
572 $feed->add_link(
573 $make_link->(
574 'self',
575 $opts->{'apilinks'}
576 ? ( 'application/x.atom+xml', "$api/feed" )
577 : ( 'text/xml', $api )
580 $feed->updated( LJ::TimeUtil->time_to_w3c($j->{'modtime'}, 'Z') );
582 my $ljinfo = $xml->createElement( 'lj:journal' );
583 $ljinfo->setAttribute( 'userid', $u->userid );
584 $ljinfo->setAttribute( 'username', LJ::exml($u->user) );
585 $ljinfo->setAttribute( 'type', LJ::exml($u->journaltype_readable) );
586 $xml->getDocumentElement->appendChild( $ljinfo );
588 # link to the AtomAPI version of this feed
589 $feed->add_link(
590 $make_link->(
591 'service.feed',
592 'application/x.atom+xml',
593 ( $opts->{'apilinks'} ? "$api/feed" : $api ),
594 $j->{'title'}
598 $feed->add_link(
599 $make_link->(
600 'service.post',
601 'application/x.atom+xml',
602 "$api/post",
603 'Create a new entry'
605 ) if $opts->{'apilinks'};
607 unless ($LJ::DISABLED{'hubbub_discovery'}) {
608 foreach my $hub (@LJ::HUBBUB_HUBS) {
609 $feed->add_link($make_link->('hub', undef, $hub));
614 my $posteru = LJ::load_userids( map { $_->{posterid} } @$cleanitems);
616 # output individual item blocks
617 foreach my $it ( @$cleanitems ) {
618 my $obj = $it->{entry};
619 my $itemid = $it->{itemid};
620 my $ditemid = $it->{ditemid};
621 my $poster = $posteru->{$it->{posterid}};
623 my $entry = XML::Atom::Entry->new( Version => 1 );
624 my $entry_xml = $entry->{doc};
626 $entry->id("urn:lj:$LJ::DOMAIN:atom1:$u->{user}:$ditemid");
628 # author isn't required if it is in the main <feed>
629 # only add author if we are in a single entry view, or
630 # the journal entry isn't owned by the journal owner. (communities)
631 if ( $opts->{'single_entry'} || $journalu->email_raw ne $poster->email_raw ) {
632 my $author = XML::Atom::Person->new( Version => 1 );
633 $author->email( $poster->email_visible ) if $poster->email_visible;
634 $author->name( $poster->{name} );
635 $entry->author( $author );
637 # and the lj-specific stuff
638 my $postauthor = $entry_xml->createElement( 'lj:poster' );
639 $postauthor->setAttribute( 'user', LJ::exml($poster->user));
640 $postauthor->setAttribute( 'userid', $poster->userid);
641 $entry_xml->getDocumentElement->appendChild( $postauthor );
644 $entry->add_link(
645 $make_link->( 'alternate', 'text/html', $obj->url ) #"$j->{'link'}$ditemid.html" )
647 $entry->add_link(
648 $make_link->( 'self', 'text/xml', "$api/?itemid=$ditemid" )
651 $entry->add_link(
652 $make_link->(
653 'service.edit', 'application/x.atom+xml',
654 "$api/edit/$itemid", 'Edit this post'
656 ) if $opts->{'apilinks'};
658 # NOTE: Atom 0.3 allowed for "issued", where we put the time the
659 # user says it was. There's no equivalent in later versions of
660 # Atom, though. And Atom 0.3 is deprecated. Oh well.
662 my ($year, $mon, $mday, $hour, $min, $sec) = split(/ /, $it->{eventtime});
663 my $event_date = sprintf("%04d-%02d-%02dT%02d:%02d:%02d",
664 $year, $mon, $mday, $hour, $min, $sec);
667 # title can't be blank and can't be absent, so we have to fake some subject
668 $entry->title( $it->{'subject'} ||
669 "$journalu->{user} \@ $event_date"
673 $entry->published( LJ::TimeUtil->time_to_w3c($it->{createtime}, "Z") );
674 $entry->updated( LJ::TimeUtil->time_to_w3c($it->{modtime}, "Z") );
676 # XML::Atom 0.13 doesn't support categories. Maybe later?
677 foreach my $tag ( @{$it->{tags} || []} ) {
678 $tag = LJ::exml( $tag );
679 my $category = $entry_xml->createElement( 'category' );
680 $category->setAttribute( 'term', $tag );
681 $category->setNamespace( $ns );
682 $entry_xml->getDocumentElement->appendChild( $category );
685 if ($it->{'music'}) {
686 my $music = $entry_xml->createElement( 'lj:music' );
687 $music->appendTextNode( $it->{'music'} );
688 $entry_xml->getDocumentElement->appendChild( $music );
691 # if syndicating the complete entry
692 # -print a content tag
693 # elsif syndicating summaries
694 # -print a summary tag
695 # else (code omitted), we're syndicating title only
696 # -print neither (the title has already been printed)
697 # note: the $event was also emptied earlier, in make_feed
699 # a lack of a content element is allowed, as long
700 # as we maintain a proper 'alternate' link (above)
701 my $make_content = sub {
702 my $content = $entry_xml->createElement( $_[0] );
703 $content->setAttribute( 'type', 'html' );
704 $content->setNamespace( $ns );
705 $content->appendTextNode( $it->{'event'} );
706 $entry_xml->getDocumentElement->appendChild( $content );
708 if ($u->{'opt_synlevel'} eq 'full') {
709 # Do this manually for now, until XML::Atom supports new
710 # content type classifications.
711 $make_content->('content');
712 } elsif ($u->{'opt_synlevel'} eq 'summary') {
713 $make_content->('summary');
716 if ( $opts->{'single_entry'} ) {
717 return $normalize_ns->( $entry->as_xml() );
719 else {
720 $feed->add_entry( $entry );
724 return $normalize_ns->( $feed->as_xml() );
727 # create a FOAF page for a user
728 sub create_view_foaf {
729 my ($journalinfo, $u, $opts) = @_;
730 my $comm = ($u->{journaltype} eq 'C');
732 my $ret;
734 # return nothing if we're not a user
735 unless ($u->{journaltype} eq 'P' || $comm) {
736 $opts->{handler_return} = 404;
737 return undef;
740 # set our content type
741 $opts->{contenttype} = 'application/rdf+xml; charset=' . $opts->{saycharset};
743 # setup userprops we will need
744 LJ::load_user_props($u, qw{
745 aolim icq yahoo jabber msn icbm url urlname external_foaf_url country city journaltitle
748 # create bare foaf document, for now
749 $ret = "<?xml version='1.0'?>\n";
750 $ret .= LJ::run_hook("bot_director", "<!-- ", " -->");
751 $ret .= "<rdf:RDF\n";
752 $ret .= " xml:lang=\"en\"\n";
753 $ret .= " xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n";
754 $ret .= " xmlns:rdfs=\"http://www.w3.org/2000/01/rdf-schema#\"\n";
755 $ret .= " xmlns:foaf=\"http://xmlns.com/foaf/0.1/\"\n";
756 $ret .= " xmlns:ya=\"http://blogs.yandex.ru/schema/foaf/\"\n";
757 $ret .= " xmlns:lj=\"http://www.livejournal.org/rss/lj/1.0/\"\n";
758 $ret .= " xmlns:geo=\"http://www.w3.org/2003/01/geo/wgs84_pos#\"\n";
759 $ret .= " xmlns:dc=\"http://purl.org/dc/elements/1.1/\">\n";
761 # precompute some values
762 my $digest = "";
763 if ($u->is_validated) {
764 my $remote = LJ::get_remote();
765 my $email_visible = $u->email_visible($remote);
766 $digest = Digest::SHA1::sha1_hex("mailto:$email_visible") if $email_visible;
769 # channel attributes
770 $ret .= ($comm ? " <foaf:Group>\n" : " <foaf:Person>\n");
771 $ret .= " <foaf:nick>$u->{user}</foaf:nick>\n";
772 $ret .= " <foaf:name>". LJ::exml($u->{name}) ."</foaf:name>\n";
773 $ret .= " <lj:journaltitle>". LJ::exml($u->{journaltitle}) ."</lj:journaltitle>\n" if $u->{journaltitle};
774 $ret .= " <lj:journalsubtitle>". LJ::exml($u->{journalsubtitle}) ."</lj:journalsubtitle>\n" if $u->{journalsubtitle};
775 $ret .= " <foaf:openid rdf:resource=\"" . $u->journal_base . "/\" />\n" unless $comm;
777 # user location
778 if ($u->{'country'}) {
779 my $ecountry = LJ::eurl($u->{'country'});
780 $ret .= " <ya:country dc:title=\"$ecountry\" rdf:resource=\"$LJ::SITEROOT/directory.bml?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry\"/>\n";
781 if ($u->{'city'}) {
782 my $estate = ''; # FIXME: add state. Yandex didn't need it.
783 my $ecity = LJ::eurl($u->{'city'});
784 $ret .= " <ya:city dc:title=\"$ecity\" rdf:resource=\"$LJ::SITEROOT/directory.bml?opt_sort=ut&amp;s_loc=1&amp;loc_cn=$ecountry&amp;loc_st=$estate&amp;loc_ci=$ecity\"/>\n";
788 if ($u->{bdate} && $u->{bdate} ne "0000-00-00" && !$comm && $u->can_show_full_bday) {
789 $ret .= " <foaf:dateOfBirth>".$u->bday_string."</foaf:dateOfBirth>\n";
791 $ret .= " <foaf:mbox_sha1sum>$digest</foaf:mbox_sha1sum>\n" if $digest;
793 # userpic
794 if (my $picid = $u->{'defaultpicid'}) {
795 $ret .= " <foaf:img rdf:resource=\"$LJ::USERPIC_ROOT/$picid/$u->{userid}\" />\n";
798 $ret .= " <foaf:page>\n";
799 $ret .= " <foaf:Document rdf:about=\"" . $u->profile_url . "\">\n";
800 $ret .= " <dc:title>$LJ::SITENAME Profile</dc:title>\n";
801 $ret .= " <dc:description>Full $LJ::SITENAME profile, including information such as interests and bio.</dc:description>\n";
802 $ret .= " </foaf:Document>\n";
803 $ret .= " </foaf:page>\n";
805 # we want to bail out if they have an external foaf file, because
806 # we want them to be able to provide their own information.
807 if ($u->{external_foaf_url}) {
808 $ret .= " <rdfs:seeAlso rdf:resource=\"" . LJ::eurl($u->{external_foaf_url}) . "\" />\n";
809 $ret .= ($comm ? " </foaf:Group>\n" : " </foaf:Person>\n");
810 $ret .= "</rdf:RDF>\n";
811 return $ret;
814 # contact type information
815 my %types = (
816 aolim => 'aimChatID',
817 icq => 'icqChatID',
818 yahoo => 'yahooChatID',
819 msn => 'msnChatID',
820 jabber => 'jabberID',
822 if ($u->{allow_contactshow} eq 'Y') {
823 foreach my $type (keys %types) {
824 next unless defined $u->{$type};
825 $ret .= " <foaf:$types{$type}>" . LJ::exml($u->{$type}) . "</foaf:$types{$type}>\n";
829 # blog activity
832 my $dbcr = LJ::get_cluster_reader($u);
833 my $num_comments_received = $u->num_comments_received( dbh => $dbcr ) || 0;
834 my $num_comments_posted = $u->num_comments_posted( dbh => $dbcr ) || 0;
836 my $count = $u->number_of_posts;
837 $ret .= " <ya:blogActivity>\n";
838 $ret .= " <ya:Posts>\n";
839 $ret .= " <ya:feed rdf:resource=\"" . LJ::journal_base($u) ."/data/rss\" dc:type=\"application/rss+xml\" />\n";
840 $ret .= " <ya:posted>$count</ya:posted>\n";
841 $ret .= " </ya:Posts>\n";
842 $ret .= " <ya:Comments>\n";
843 ##### we are don't have rss feed for user's comments
844 #### $ret .= " <ya:feed rdf:resource=\"recent comments rss\" dc:type=\"application/rss+xml\"/>\n";
845 ###############
846 $ret .= " <ya:posted>$num_comments_posted</ya:posted>\n";
847 $ret .= " <ya:received>$num_comments_received</ya:received>\n";
848 $ret .= " </ya:Comments>\n";
849 $ret .= " </ya:blogActivity>\n";
852 # include a user's journal page and web site info
853 my $time_create = ($u->timecreate) ? LJ::TimeUtil->time_to_w3c($u->timecreate) : '';
854 my $time_update = ($u->timeupdate) ? LJ::TimeUtil->time_to_w3c($u->timeupdate) : '';
855 $ret .= " <foaf:weblog rdf:resource='" . LJ::journal_base($u) . "/'";
856 $ret .= " lj:dateCreated='$time_create'" if $time_create;
857 $ret .= " lj:dateLastUpdated='$time_update'" if $time_update;
858 $ret .= " />\n";
859 if ($u->{url}) {
860 $ret .= " <foaf:homepage rdf:resource=\"" . LJ::eurl($u->{url});
861 $ret .= "\" dc:title=\"" . LJ::exml($u->{urlname}) . "\" />\n";
864 # user bio
865 if ($u->{'has_bio'} eq "Y") {
866 $u->{'bio'} = LJ::get_bio($u);
867 LJ::text_out(\$u->{'bio'});
868 LJ::CleanHTML::clean_userbio(\$u->{'bio'});
869 $ret .= " <ya:bio>" . LJ::exml($u->{'bio'}) . "</ya:bio>\n";
872 # user schools
873 if ($u->{'journaltype'} ne 'Y' &&
874 !$LJ::DISABLED{'schools'} &&
875 ($u->{'opt_showschools'} eq '' || $u->{'opt_showschools'} eq 'Y')) {
877 my $schools = LJ::Schools::get_attended($u);
878 if ($u->{'journaltype'} ne 'C' && $schools && %$schools ) {
879 my @links;
880 foreach my $sid (sort { $schools->{$a}->{year_start} <=> $schools->{$b}->{year_start} } keys %$schools) {
881 my $link = "$LJ::SITEROOT/schools/" .
882 "?ctc=" . LJ::eurl($schools->{$sid}->{country}) .
883 "&sc=" . LJ::eurl($schools->{$sid}->{state}) .
884 "&cc=" . LJ::eurl($schools->{$sid}->{city}) .
885 "&sid=" . $sid ;
886 my $ename = LJ::ehtml($schools->{$sid}->{name});
887 $ret .= " <ya:school\n";
888 $ret .= " rdf:resource=\"" . LJ::exml($link) . "\"\n";
889 if (defined $schools->{$sid}->{year_start}) {
890 $ret .= " ya:dateStart=\"$schools->{$sid}->{year_start}\"\n";
892 if (defined $schools->{$sid}->{year_end}) {
893 $ret .= " ya:dateFinish=\"$schools->{$sid}->{year_end}\"\n";
896 $ret .= " dc:title=\"$ename\"/>\n";
901 # icbm/location info
902 if ($u->{icbm}) {
903 my @loc = split(",", $u->{icbm});
904 $ret .= " <foaf:based_near><geo:Point geo:lat='" . $loc[0] . "'" .
905 " geo:long='" . $loc[1] . "' /></foaf:based_near>\n";
908 # interests, please!
909 # arrayref of interests rows: [ intid, intname, intcount ]
910 my $intu = LJ::get_interests($u);
911 foreach my $int (@$intu) {
912 LJ::text_out(\$int->[1]); # 1==interest
913 $ret .= " <foaf:interest dc:title=\"". LJ::exml($int->[1]) . "\" " .
914 "rdf:resource=\"$LJ::SITEROOT/interests.bml?int=" . LJ::eurl($int->[1]) . "\" />\n";
917 # check if the user has a "FOAF-knows" group
918 my $groups = LJ::get_friend_group($u->{userid}, { name => 'FOAF-knows' });
919 my $mask = $groups ? 1 << $groups->{groupnum} : 0;
921 # now information on who you know, limited to a certain maximum number of users
922 my $friends = LJ::get_friends($u->{userid}, $mask);
923 my @ids = keys %$friends;
924 @ids = splice(@ids, 0, $LJ::MAX_FOAF_FRIENDS) if @ids > $LJ::MAX_FOAF_FRIENDS;
926 # now load
927 my %users;
928 LJ::load_userids_multiple([ map { $_, \$users{$_} } @ids ], [$u]);
930 # iterate to create data structure
931 foreach my $friendid (@ids) {
932 next if $friendid == $u->{userid};
933 my $fu = $users{$friendid};
934 next if $fu->{statusvis} =~ /[DXS]/ || $fu->{journaltype} ne 'P';
936 my $name = LJ::exml($fu->name_raw);
937 my $tagline = LJ::exml($fu->prop('journaltitle') || '');
938 my $upicurl = $fu->userpic ? $fu->userpic->url : '';
940 $ret .= $comm ? " <foaf:member>\n" : " <foaf:knows>\n";
941 $ret .= " <foaf:Person>\n";
942 $ret .= " <foaf:nick>$fu->{'user'}</foaf:nick>\n";
943 $ret .= " <foaf:member_name>$name</foaf:member_name>\n";
944 $ret .= " <foaf:tagLine>$tagline</foaf:tagLine>\n";
945 $ret .= " <foaf:image>$upicurl</foaf:image>\n" if $upicurl;
946 $ret .= " <rdfs:seeAlso rdf:resource=\"" . LJ::journal_base($fu) ."/data/foaf\" />\n";
947 $ret .= " <foaf:weblog rdf:resource=\"" . LJ::journal_base($fu) . "/\"/>\n";
948 $ret .= " </foaf:Person>\n";
949 $ret .= $comm ? " </foaf:member>\n" : " </foaf:knows>\n";
952 # finish off the document
953 $ret .= $comm ? " </foaf:Group>\n" : " </foaf:Person>\n";
954 $ret .= "</rdf:RDF>\n";
956 return $ret;
959 # YADIS capability discovery
960 sub create_view_yadis {
961 my ($journalinfo, $u, $opts) = @_;
962 my $person = ($u->{journaltype} eq 'P');
964 my $ret = "";
966 my $println = sub { $ret .= $_[0]."\n"; };
968 $println->('<?xml version="1.0" encoding="UTF-8"?>');
969 $println->('<xrds:XRDS xmlns:xrds="xri://$xrds" xmlns="xri://$xrd*($v*2.0)"><XRD>');
971 local $1;
972 $opts->{pathextra} =~ m!^(/.*)?$!;
973 my $viewchunk = $1;
975 my $view;
976 if ($viewchunk eq '') {
977 $view = "recent";
979 elsif ($viewchunk eq '/friends') {
980 $view = "friends";
982 else {
983 $view = undef;
986 if ($view eq 'recent') {
987 # Only people (not communities, etc) can be OpenID authenticated
988 if ($person && LJ::OpenID->server_enabled) {
989 $println->(' <Service priority="0">');
990 $println->(' <Type>http://specs.openid.net/auth/2.0/signon</Type>');
991 $println->(' <URI>'.LJ::ehtml($LJ::OPENID_SERVER).'</URI>');
992 $println->(' <LocalID>'.LJ::ehtml($u->journal_base) . '/' .'</LocalID>');
993 $println->(' </Service>');
996 elsif ($view eq 'friends') {
997 $println->(' <Service xmlns:gm="http://openid.net/xmlns/groupmembership/xrds">');
998 $println->(' <Type>http://openid.net/xmlns/groupmembership</Type>');
999 $println->(' <URI>'.LJ::exml($LJ::SITEROOT).'/openid/groupmembership.bml</URI>');
1000 $println->(' <LocalID>'.LJ::exml($u->journal_base.'/friends').'</LocalID>');
1001 $println->(' <gm:CanEnumerate /><gm:CanQuery />');
1002 $println->(' </Service>');
1005 # Local site-specific content
1006 # TODO: Give these hooks access to $view somehow?
1007 LJ::run_hook("yadis_service_descriptors", \$ret);
1009 $println->('</XRD></xrds:XRDS>');
1010 return $ret;
1013 # create a userpic page for a user
1014 sub create_view_userpics {
1015 my ($journalinfo, $u, $opts) = @_;
1016 my ( $feed, $xml, $ns );
1018 $ns = "http://www.w3.org/2005/Atom";
1020 my $normalize_ns = sub {
1021 my $str = shift;
1022 $str =~ s/(<\w+)\s+xmlns="\Q$ns\E"/$1/og;
1023 $str =~ s/<feed\b/<feed xmlns="$ns"/;
1024 return $str;
1027 my $make_link = sub {
1028 my ( $rel, $type, $href, $title ) = @_;
1029 my $link = XML::Atom::Link->new( Version => 1 );
1030 $link->rel($rel);
1031 $link->type($type);
1032 $link->href($href);
1033 $link->title( $title ) if $title;
1034 return $link;
1037 my $author = XML::Atom::Person->new( Version => 1 );
1038 $author->name( $u->{name} );
1040 $feed = XML::Atom::Feed->new( Version => 1 );
1041 $xml = $feed->{doc};
1043 if ($u->should_block_robots) {
1044 $xml->getDocumentElement->setAttribute( "xmlns:idx", "urn:atom-extension:indexing" );
1045 $xml->getDocumentElement->setAttribute( "idx:index", "no" );
1048 my $bot = LJ::run_hook("bot_director");
1049 $xml->insertBefore( $xml->createComment( $bot ), $xml->documentElement())
1050 if $bot;
1052 $feed->id( "urn:lj:$LJ::DOMAIN:atom1:$u->{user}:userpics" );
1053 $feed->title( "$u->{user}'s userpics" );
1055 $feed->author( $author );
1056 $feed->add_link( $make_link->( 'alternate', 'text/html', "$LJ::SITEROOT/allpics.bml?user=$u->{user}" ) );
1057 $feed->add_link( $make_link->( 'self', 'text/xml', $u->journal_base() . "/data/userpics" ) );
1059 # now start building all the userpic data
1060 # start up by loading all of our userpic information and creating that part of the feed
1061 my $info = LJ::get_userpic_info($u, {'load_comments' => 1, 'load_urls' => 1});
1063 my %keywords = ();
1064 while (my ($kw, $pic) = each %{$info->{kw}}) {
1065 LJ::text_out(\$kw);
1066 push @{$keywords{$pic->{picid}}}, LJ::ehtml($kw);
1069 my %comments = ();
1070 while (my ($pic, $comment) = each %{$info->{comment}}) {
1071 LJ::text_out(\$comment);
1072 $comments{$pic} = LJ::ehtml($comment);
1075 my @pics;
1076 push @pics, map { $info->{pic}->{$_} } sort { $a <=> $b }
1077 grep { $info->{pic}->{$_}->{state} eq 'N' } keys %{$info->{pic}};
1079 my $entry;
1080 my %picdata;
1082 # this is lame, but we have to do this iteration twice; we load the userpic data first, so that
1083 # we can figure out what the most recently-uploaded userpic is. we need to put that into the feed
1084 # before any of the <entry> values.
1086 my $latest = 0;
1087 foreach my $pic (@pics) {
1088 LJ::load_userpics(\%picdata, [$u, $pic->{picid}] );
1089 $latest = ($latest < $picdata{$pic->{picid}}->{picdate}) ? $picdata{$pic->{picid}}->{picdate} : $latest;
1092 $feed->updated( LJ::TimeUtil->time_to_w3c($latest, 'Z') );
1094 foreach my $pic (@pics) {
1095 my $entry = XML::Atom::Entry->new( Version => 1 );
1096 my $entry_xml = $entry->{doc};
1098 $entry->id("urn:lj:$LJ::DOMAIN:atom1:$u->{user}:userpics:$pic->{picid}");
1100 my $title = ($pic->{picid} == $u->{defaultpicid}) ? "default userpic" : "userpic";
1101 $entry->title( $title );
1103 $entry->updated( LJ::TimeUtil->time_to_w3c($picdata{$pic->{picid}}->{picdate}, 'Z') );
1105 my $content;
1106 $content = $entry_xml->createElement( "content" );
1107 $content->setAttribute( 'src', "$LJ::USERPIC_ROOT/$pic->{picid}/$u->{userid}" );
1108 $content->setNamespace( $ns );
1109 $entry_xml->getDocumentElement->appendChild( $content );
1111 foreach my $kw (@{$keywords{$pic->{picid}}}) {
1112 my $ekw = LJ::exml( $kw );
1113 my $category = $entry_xml->createElement( 'category' );
1114 $category->setAttribute( 'term', $ekw );
1115 $category->setNamespace( $ns );
1116 $entry_xml->getDocumentElement->appendChild( $category );
1119 if($comments{$pic->{picid}}) {
1120 my $content = $entry_xml->createElement( "summary" );
1121 $content->setNamespace( $ns );
1122 $content->appendTextNode( $comments{$pic->{picid}} );
1123 $entry_xml->getDocumentElement->appendChild( $content );
1126 $feed->add_entry( $entry );
1129 return $normalize_ns->( $feed->as_xml() );
1133 sub create_view_comments
1135 my ($journalinfo, $u, $opts) = @_;
1137 if (LJ::conf_test($LJ::DISABLED{latest_comments_rss}, $u)) {
1138 $opts->{handler_return} = 404;
1139 return 404;
1142 unless ($u->get_cap('latest_comments_rss')) {
1143 $opts->{handler_return} = 403;
1144 return;
1147 my $ret;
1148 $ret .= "<?xml version='1.0' encoding='$opts->{'saycharset'}' ?>\n";
1149 $ret .= LJ::run_hook("bot_director", "<!-- ", " -->") . "\n";
1150 $ret .= "<rss version='2.0' xmlns:lj='http://www.livejournal.org/rss/lj/1.0/'>\n";
1152 # channel attributes
1153 $ret .= "<channel>\n";
1154 $ret .= " <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
1155 $ret .= " <link>$journalinfo->{link}</link>\n";
1156 $ret .= " <description>Latest comments in " . LJ::exml($journalinfo->{title}) . "</description>\n";
1157 $ret .= " <managingEditor>" . LJ::exml($journalinfo->{email}) . "</managingEditor>\n" if $journalinfo->{email};
1158 $ret .= " <lastBuildDate>$journalinfo->{builddate}</lastBuildDate>\n";
1159 $ret .= " <generator>LiveJournal / $LJ::SITENAME</generator>\n";
1160 $ret .= " <lj:journal>" . $u->user . "</lj:journal>\n";
1161 $ret .= " <lj:journaltype>" . $u->journaltype_readable . "</lj:journaltype>\n";
1162 # TODO: add 'language' field when user.lang has more useful information
1164 ### image block, returns info for their current userpic
1165 if ($u->{'defaultpicid'}) {
1166 my $pic = {};
1167 LJ::load_userpics($pic, [ $u, $u->{'defaultpicid'} ]);
1168 $pic = $pic->{$u->{'defaultpicid'}}; # flatten
1170 $ret .= " <image>\n";
1171 $ret .= " <url>$LJ::USERPIC_ROOT/$u->{'defaultpicid'}/$u->{'userid'}</url>\n";
1172 $ret .= " <title>" . LJ::exml($journalinfo->{title}) . "</title>\n";
1173 $ret .= " <link>$journalinfo->{link}</link>\n";
1174 $ret .= " <width>$pic->{'width'}</width>\n";
1175 $ret .= " <height>$pic->{'height'}</height>\n";
1176 $ret .= " </image>\n\n";
1179 my @comments = $u->get_recent_talkitems(25);
1180 foreach my $r (@comments)
1182 my $c = LJ::Comment->new($u, jtalkid => $r->{jtalkid});
1183 my $thread_url = $c->thread_url;
1184 my $subject = $c->subject_raw;
1185 LJ::CleanHTML::clean_subject_all(\$subject);
1187 $ret .= "<item>\n";
1188 $ret .= " <guid isPermaLink='true'>$thread_url</guid>\n";
1189 $ret .= " <pubDate>" . LJ::TimeUtil->time_to_http($r->{datepostunix}) . "</pubDate>\n";
1190 $ret .= " <title>" . LJ::exml($subject) . "</title>\n" if $subject;
1191 $ret .= " <link>$thread_url</link>\n";
1192 # omit the description tag if we're only syndicating titles
1193 unless ($u->{'opt_synlevel'} eq 'title') {
1194 my $body = $c->body_raw;
1195 LJ::CleanHTML::clean_subject_all(\$body);
1196 $ret .= " <description>" . LJ::exml($body) . "</description>\n";
1198 $ret .= "</item>\n";
1201 $ret .= "</channel>\n";
1202 $ret .= "</rss>\n";
1205 return $ret;
1208 sub generate_hubbub_jobs {
1209 my $u = shift;
1210 my $joblist = shift;
1212 return if $LJ::DISABLED{'hubbub'};
1214 foreach my $hub (@LJ::HUBBUB_HUBS) {
1215 my $make_hubbub_job = sub {
1216 my $type = shift;
1218 my $topic_url = $u->journal_base . "/data/$type";
1219 return TheSchwartz::Job->new(
1220 funcname => 'TheSchwartz::Worker::PubSubHubbubPublish',
1221 arg => {
1222 hub => $hub,
1223 topic_url => $topic_url,
1225 coalesce => $hub,
1229 push @$joblist, $make_hubbub_job->("rss");
1230 push @$joblist, $make_hubbub_job->("atom");