We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

apiaction.php 46.1 KB
Newer Older
1
<?php
2 3
/**
 * StatusNet, the distributed open-source microblogging tool
4
 *
5 6 7 8 9
 * Base API action
 *
 * PHP version 5
 *
 * LICENCE: This program is free software: you can redistribute it and/or modify
10 11 12 13 14 15 16 17 18 19 20
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
21 22 23
 *
 * @category  API
 * @package   StatusNet
Zach Copley's avatar
Zach Copley committed
24 25 26 27 28
 * @author    Craig Andrews <candrews@integralblue.com>
 * @author    Dan Moore <dan@moore.cx>
 * @author    Evan Prodromou <evan@status.net>
 * @author    Jeffery To <jeffery.to@gmail.com>
 * @author    Toby Inkster <mail@tobyinkster.co.uk>
29 30 31 32
 * @author    Zach Copley <zach@status.net>
 * @copyright 2009 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link      http://status.net/
33 34
 */

35 36 37 38 39 40 41 42 43 44 45 46
/* External API usage documentation. Please update when you change how the API works. */

/*! @mainpage StatusNet REST API

    @section Introduction

    Some explanatory text about the API would be nice.

    @section API Methods

    @subsection timelinesmethods_sec Timeline Methods

Zach Copley's avatar
Zach Copley committed
47
    @li @ref publictimeline
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95
    @li @ref friendstimeline

    @subsection statusmethods_sec Status Methods

    @li @ref statusesupdate

    @subsection usermethods_sec User Methods

    @subsection directmessagemethods_sec Direct Message Methods

    @subsection friendshipmethods_sec Friendship Methods

    @subsection socialgraphmethods_sec Social Graph Methods

    @subsection accountmethods_sec Account Methods

    @subsection favoritesmethods_sec Favorites Methods

    @subsection blockmethods_sec Block Methods

    @subsection oauthmethods_sec OAuth Methods

    @subsection helpmethods_sec Help Methods

    @subsection groupmethods_sec Group Methods

    @page apiroot API Root

    The URLs for methods referred to in this API documentation are
    relative to the StatusNet API root. The API root is determined by the
    site's @b server and @b path variables, which are generally specified
    in config.php. For example:

    @code
    $config['site']['server'] = 'example.org';
    $config['site']['path'] = 'statusnet'
    @endcode

    The pattern for a site's API root is: @c protocol://server/path/api E.g:

    @c http://example.org/statusnet/api

    The @b path can be empty.  In that case the API root would simply be:

    @c http://example.org/api

*/

96
if (!defined('STATUSNET')) {
97 98
    exit(1);
}
99

100 101 102 103 104
/**
 * Contains most of the Twitter-compatible API output functions.
 *
 * @category API
 * @package  StatusNet
Zach Copley's avatar
Zach Copley committed
105 106 107 108 109
 * @author   Craig Andrews <candrews@integralblue.com>
 * @author   Dan Moore <dan@moore.cx>
 * @author   Evan Prodromou <evan@status.net>
 * @author   Jeffery To <jeffery.to@gmail.com>
 * @author   Toby Inkster <mail@tobyinkster.co.uk>
110 111 112 113 114 115
 * @author   Zach Copley <zach@status.net>
 * @license  http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
 * @link     http://status.net/
 */

class ApiAction extends Action
116
{
117 118 119
    const READ_ONLY  = 1;
    const READ_WRITE = 2;

120 121 122 123 124 125 126
    var $format    = null;
    var $user      = null;
    var $auth_user = null;
    var $page      = null;
    var $count     = null;
    var $max_id    = null;
    var $since_id  = null;
127

128 129
    var $access    = self::READ_ONLY;  // read (default) or read-write

130 131 132 133 134
    /**
     * Initialization.
     *
     * @param array $args Web and URL arguments
     *
135
     * @return boolean false if user doesn't exist
136 137 138 139
     */

    function prepare($args)
    {
140
        StatusNet::setApi(true); // reduce exception reports to aid in debugging
141
        parent::prepare($args);
142

143
        $this->format   = $this->arg('format');
144 145 146 147
        $this->page     = (int)$this->arg('page', 1);
        $this->count    = (int)$this->arg('count', 20);
        $this->max_id   = (int)$this->arg('max_id', 0);
        $this->since_id = (int)$this->arg('since_id', 0);
148 149

        if ($this->arg('since')) {
150
            header('X-StatusNet-Warning: since parameter is disabled; use since_id');
151
        }
152

153 154 155 156 157 158 159 160 161 162 163
        return true;
    }

    /**
     * Handle a request
     *
     * @param array $args Arguments from $_REQUEST
     *
     * @return void
     */

164 165
    function handle($args)
    {
166
        header('Access-Control-Allow-Origin: *');
167 168
        parent::handle($args);
    }
169

170 171 172
    /**
     * Overrides XMLOutputter::element to write booleans as strings (true|false).
     * See that method's documentation for more info.
173
     *
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
     * @param string $tag     Element type or tagname
     * @param array  $attrs   Array of element attributes, as
     *                        key-value pairs
     * @param string $content string content of the element
     *
     * @return void
     */
    function element($tag, $attrs=null, $content=null)
    {
        if (is_bool($content)) {
            $content = ($content ? 'true' : 'false');
        }

        return parent::element($tag, $attrs, $content);
    }
189

190
    function twitterUserArray($profile, $get_notice=false)
191
    {
192
        $twitter_user = array();
193

194
        $twitter_user['id'] = intval($profile->id);
195 196
        $twitter_user['name'] = $profile->getBestName();
        $twitter_user['screen_name'] = $profile->nickname;
Evan Prodromou's avatar
Evan Prodromou committed
197
        $twitter_user['location'] = ($profile->location) ? $profile->location : null;
198
        $twitter_user['description'] = ($profile->bio) ? $profile->bio : null;
199

200
        $avatar = $profile->getAvatar(AVATAR_STREAM_SIZE);
201 202
        $twitter_user['profile_image_url'] = ($avatar) ? $avatar->displayUrl() :
            Avatar::defaultImage(AVATAR_STREAM_SIZE);
203

Evan Prodromou's avatar
Evan Prodromou committed
204
        $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null;
205
        $twitter_user['protected'] = false; # not supported by StatusNet yet
206
        $twitter_user['followers_count'] = $profile->subscriberCount();
207

208
        $design        = null;
209
        $user          = $profile->getUser();
210

211
        // Note: some profiles don't have an associated user
212

213 214
        $defaultDesign = Design::siteDesign();

215 216 217 218
        if (!empty($user)) {
            $design = $user->getDesign();
        }

219
        if (empty($design)) {
220
            $design = $defaultDesign;
221 222
        }

223 224 225 226 227 228 229 230
        $color = Design::toWebColor(empty($design->backgroundcolor) ? $defaultDesign->backgroundcolor : $design->backgroundcolor);
        $twitter_user['profile_background_color'] = ($color == null) ? '' : '#'.$color->hexValue();
        $color = Design::toWebColor(empty($design->textcolor) ? $defaultDesign->textcolor : $design->textcolor);
        $twitter_user['profile_text_color'] = ($color == null) ? '' : '#'.$color->hexValue();
        $color = Design::toWebColor(empty($design->linkcolor) ? $defaultDesign->linkcolor : $design->linkcolor);
        $twitter_user['profile_link_color'] = ($color == null) ? '' : '#'.$color->hexValue();
        $color = Design::toWebColor(empty($design->sidebarcolor) ? $defaultDesign->sidebarcolor : $design->sidebarcolor);
        $twitter_user['profile_sidebar_fill_color'] = ($color == null) ? '' : '#'.$color->hexValue();
231 232
        $twitter_user['profile_sidebar_border_color'] = '';

233
        $twitter_user['friends_count'] = $profile->subscriptionCount();
234

235
        $twitter_user['created_at'] = $this->dateTwitter($profile->created);
236

237
        $twitter_user['favourites_count'] = $profile->faveCount(); // British spelling!
238 239 240

        $timezone = 'UTC';

241
        if (!empty($user) && $user->timezone) {
242 243 244 245 246 247 248 249 250
            $timezone = $user->timezone;
        }

        $t = new DateTime;
        $t->setTimezone(new DateTimeZone($timezone));

        $twitter_user['utc_offset'] = $t->format('Z');
        $twitter_user['time_zone'] = $timezone;

251 252 253 254 255 256 257 258
        $twitter_user['profile_background_image_url']
            = empty($design->backgroundimage)
            ? '' : ($design->disposition & BACKGROUND_ON)
            ? Design::url($design->backgroundimage) : '';

        $twitter_user['profile_background_tile']
            = empty($design->disposition)
            ? '' : ($design->disposition & BACKGROUND_TILE) ? 'true' : 'false';
259

260
        $twitter_user['statuses_count'] = $profile->noticeCount();
261 262 263 264 265

        // Is the requesting user following this user?
        $twitter_user['following'] = false;
        $twitter_user['notifications'] = false;

266
        if (isset($this->auth_user)) {
267

268
            $twitter_user['following'] = $this->auth_user->isSubscribed($profile);
269 270 271

            // Notifications on?
            $sub = Subscription::pkeyGet(array('subscriber' =>
272 273
                                               $this->auth_user->id,
                                               'subscribed' => $profile->id));
274 275 276 277 278

            if ($sub) {
                $twitter_user['notifications'] = ($sub->jabber || $sub->sms);
            }
        }
Evan Prodromou's avatar
Evan Prodromou committed
279

280 281 282
        if ($get_notice) {
            $notice = $profile->getCurrentNotice();
            if ($notice) {
283
                # don't get user!
284
                $twitter_user['status'] = $this->twitterStatusArray($notice, false);
285 286
            }
        }
287

288 289
        return $twitter_user;
    }
290

291
    function twitterStatusArray($notice, $include_user=true)
292 293 294
    {
        $base = $this->twitterSimpleStatusArray($notice, $include_user);

295
        if (!empty($notice->repeat_of)) {
296
            $original = Notice::staticGet('id', $notice->repeat_of);
297
            if (!empty($original)) {
298
                $original_array = $this->twitterSimpleStatusArray($original, $include_user);
299
                $base['retweeted_status'] = $original_array;
300
            }
301
        }
302 303

        return $base;
304 305 306
    }

    function twitterSimpleStatusArray($notice, $include_user=true)
307
    {
308
        $profile = $notice->getProfile();
309

310 311
        $twitter_status = array();
        $twitter_status['text'] = $notice->content;
312
        $twitter_status['truncated'] = false; # Not possible on StatusNet
313
        $twitter_status['created_at'] = $this->dateTwitter($notice->created);
314 315
        $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ?
            intval($notice->reply_to) : null;
316 317 318 319 320 321

        $source = null;

        $ns = $notice->getSource();
        if ($ns) {
            if (!empty($ns->name) && !empty($ns->url)) {
322
                $source = '<a href="' . $ns->url . '" rel="nofollow">' . $ns->name . '</a>';
323 324 325 326 327
            } else {
                $source = $ns->code;
            }
        }

328
        $twitter_status['source'] = htmlentities($source);
329
        $twitter_status['id'] = intval($notice->id);
330 331 332 333 334 335 336 337 338 339 340 341 342 343

        $replier_profile = null;

        if ($notice->reply_to) {
            $reply = Notice::staticGet(intval($notice->reply_to));
            if ($reply) {
                $replier_profile = $reply->getProfile();
            }
        }

        $twitter_status['in_reply_to_user_id'] =
            ($replier_profile) ? intval($replier_profile->id) : null;
        $twitter_status['in_reply_to_screen_name'] =
            ($replier_profile) ? $replier_profile->nickname : null;
344

345 346 347 348 349 350 351 352 353
        if (isset($notice->lat) && isset($notice->lon)) {
            // This is the format that GeoJSON expects stuff to be in
            $twitter_status['geo'] = array('type' => 'Point',
                                           'coordinates' => array((float) $notice->lat,
                                                                  (float) $notice->lon));
        } else {
            $twitter_status['geo'] = null;
        }

354
        if (isset($this->auth_user)) {
355
            $twitter_status['favorited'] = $this->auth_user->hasFave($notice);
356
        } else {
357
            $twitter_status['favorited'] = false;
358
        }
Evan Prodromou's avatar
Evan Prodromou committed
359

360
        // Enclosures
361 362
        $attachments = $notice->attachments();

363 364 365 366 367
        if (!empty($attachments)) {

            $twitter_status['attachments'] = array();

            foreach ($attachments as $attachment) {
368
                $enclosure_o=$attachment->getEnclosure();
369
                if ($enclosure_o) {
370
                    $enclosure = array();
371 372 373
                    $enclosure['url'] = $enclosure_o->url;
                    $enclosure['mimetype'] = $enclosure_o->mimetype;
                    $enclosure['size'] = $enclosure_o->size;
374
                    $twitter_status['attachments'][] = $enclosure;
375 376
                }
            }
377 378
        }

379
        if ($include_user && $profile) {
380
            # Don't get notice (recursive!)
381
            $twitter_user = $this->twitterUserArray($profile, false);
382 383
            $twitter_status['user'] = $twitter_user;
        }
384

385 386
        return $twitter_status;
    }
387

388
    function twitterGroupArray($group)
389 390 391
    {
        $twitter_group=array();
        $twitter_group['id']=$group->id;
392
        $twitter_group['url']=$group->permalink();
393 394 395 396 397 398 399 400 401
        $twitter_group['nickname']=$group->nickname;
        $twitter_group['fullname']=$group->fullname;
        $twitter_group['original_logo']=$group->original_logo;
        $twitter_group['homepage_logo']=$group->homepage_logo;
        $twitter_group['stream_logo']=$group->stream_logo;
        $twitter_group['mini_logo']=$group->mini_logo;
        $twitter_group['homepage']=$group->homepage;
        $twitter_group['description']=$group->description;
        $twitter_group['location']=$group->location;
402 403
        $twitter_group['created']=$this->dateTwitter($group->created);
        $twitter_group['modified']=$this->dateTwitter($group->modified);
404 405 406
        return $twitter_group;
    }

407
    function twitterRssGroupArray($group)
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424
    {
        $entry = array();
        $entry['content']=$group->description;
        $entry['title']=$group->nickname;
        $entry['link']=$group->permalink();
        $entry['published']=common_date_iso8601($group->created);
        $entry['updated']==common_date_iso8601($group->modified);
        $taguribase = common_config('integration', 'groupuri');
        $entry['id'] = "group:$groupuribase:$entry[link]";

        $entry['description'] = $entry['content'];
        $entry['pubDate'] = common_date_rfc2822($group->created);
        $entry['guid'] = $entry['link'];

        return $entry;
    }

425
    function twitterRssEntryArray($notice)
426
    {
427 428
        $profile = $notice->getProfile();
        $entry = array();
429

430
        // We trim() to avoid extraneous whitespace in the output
431

432 433 434 435
        $entry['content'] = common_xml_safe_str(trim($notice->rendered));
        $entry['title'] = $profile->nickname . ': ' . common_xml_safe_str(trim($notice->content));
        $entry['link'] = common_local_url('shownotice', array('notice' => $notice->id));
        $entry['published'] = common_date_iso8601($notice->created);
436

437
        $taguribase = TagURI::base();
438 439
        $entry['id'] = "tag:$taguribase:$entry[link]";

440
        $entry['updated'] = $entry['published'];
441
        $entry['author'] = $profile->getBestName();
442

443 444 445 446 447
        // Enclosures
        $attachments = $notice->attachments();
        $enclosures = array();

        foreach ($attachments as $attachment) {
448 449
            $enclosure_o=$attachment->getEnclosure();
            if ($enclosure_o) {
450
                 $enclosure = array();
451 452 453
                 $enclosure['url'] = $enclosure_o->url;
                 $enclosure['mimetype'] = $enclosure_o->mimetype;
                 $enclosure['size'] = $enclosure_o->size;
454 455 456 457 458 459 460 461
                 $enclosures[] = $enclosure;
            }
        }

        if (!empty($enclosures)) {
            $entry['enclosures'] = $enclosures;
        }

462 463 464 465 466 467 468 469 470 471 472
        // Tags/Categories
        $tag = new Notice_tag();
        $tag->notice_id = $notice->id;
        if ($tag->find()) {
            $entry['tags']=array();
            while ($tag->fetch()) {
                $entry['tags'][]=$tag->tag;
            }
        }
        $tag->free();

473
        // RSS Item specific
474 475 476 477
        $entry['description'] = $entry['content'];
        $entry['pubDate'] = common_date_rfc2822($notice->created);
        $entry['guid'] = $entry['link'];

478 479 480 481 482 483 484 485 486 487
        if (isset($notice->lat) && isset($notice->lon)) {
            // This is the format that GeoJSON expects stuff to be in.
            // showGeoRSS() below uses it for XML output, so we reuse it
            $entry['geo'] = array('type' => 'Point',
                                  'coordinates' => array((float) $notice->lat,
                                                         (float) $notice->lon));
        } else {
            $entry['geo'] = null;
        }

488 489 490
        return $entry;
    }

491
    function twitterRelationshipArray($source, $target)
492 493 494 495
    {
        $relationship = array();

        $relationship['source'] =
496
            $this->relationshipDetailsArray($source, $target);
497
        $relationship['target'] =
498
            $this->relationshipDetailsArray($target, $source);
499 500 501 502

        return array('relationship' => $relationship);
    }

503
    function relationshipDetailsArray($source, $target)
504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529
    {
        $details = array();

        $details['screen_name'] = $source->nickname;
        $details['followed_by'] = $target->isSubscribed($source);
        $details['following'] = $source->isSubscribed($target);

        $notifications = false;

        if ($source->isSubscribed($target)) {

            $sub = Subscription::pkeyGet(array('subscriber' =>
                $source->id, 'subscribed' => $target->id));

            if (!empty($sub)) {
                $notifications = ($sub->jabber || $sub->sms);
            }
        }

        $details['notifications_enabled'] = $notifications;
        $details['blocking'] = $source->hasBlocked($target);
        $details['id'] = $source->id;

        return $details;
    }

530
    function showTwitterXmlRelationship($relationship)
531 532 533 534 535 536
    {
        $this->elementStart('relationship');

        foreach($relationship as $element => $value) {
            if ($element == 'source' || $element == 'target') {
                $this->elementStart($element);
537
                $this->showXmlRelationshipDetails($value);
538 539 540 541 542 543 544
                $this->elementEnd($element);
            }
        }

        $this->elementEnd('relationship');
    }

545
    function showXmlRelationshipDetails($details)
546 547 548 549 550 551
    {
        foreach($details as $element => $value) {
            $this->element($element, null, $value);
        }
    }

552
    function showTwitterXmlStatus($twitter_status, $tag='status')
553
    {
554
        $this->elementStart($tag);
555 556 557
        foreach($twitter_status as $element => $value) {
            switch ($element) {
            case 'user':
558
                $this->showTwitterXmlUser($twitter_status['user']);
559 560
                break;
            case 'text':
561
                $this->element($element, null, common_xml_safe_str($value));
562
                break;
563
            case 'attachments':
564
                $this->showXmlAttachments($twitter_status['attachments']);
565
                break;
566
            case 'geo':
567
                $this->showGeoXML($value);
568
                break;
569 570 571
            case 'retweeted_status':
                $this->showTwitterXmlStatus($value, 'retweeted_status');
                break;
572
            default:
573
                $this->element($element, null, $value);
574 575
            }
        }
576
        $this->elementEnd($tag);
577 578
    }

579
    function showTwitterXmlGroup($twitter_group)
580 581 582 583 584 585 586 587
    {
        $this->elementStart('group');
        foreach($twitter_group as $element => $value) {
            $this->element($element, null, $value);
        }
        $this->elementEnd('group');
    }

588
    function showTwitterXmlUser($twitter_user, $role='user')
589
    {
590
        $this->elementStart($role);
591 592
        foreach($twitter_user as $element => $value) {
            if ($element == 'status') {
593
                $this->showTwitterXmlStatus($twitter_user['status']);
594
            } else {
595
                $this->element($element, null, $value);
596 597
            }
        }
598
        $this->elementEnd($role);
599 600
    }

601
    function showXmlAttachments($attachments) {
602 603 604 605 606 607 608 609 610 611 612 613 614
        if (!empty($attachments)) {
            $this->elementStart('attachments', array('type' => 'array'));
            foreach ($attachments as $attachment) {
                $attrs = array();
                $attrs['url'] = $attachment['url'];
                $attrs['mimetype'] = $attachment['mimetype'];
                $attrs['size'] = $attachment['size'];
                $this->element('enclosure', $attrs, '');
            }
            $this->elementEnd('attachments');
        }
    }

615 616 617 618 619 620 621 622 623 624 625 626
    function showGeoXML($geo)
    {
        if (empty($geo)) {
            // empty geo element
            $this->element('geo');
        } else {
            $this->elementStart('geo', array('xmlns:georss' => 'http://www.georss.org/georss'));
            $this->element('georss:point', null, $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]);
            $this->elementEnd('geo');
        }
    }

627 628
    function showGeoRSS($geo)
    {
629 630 631 632 633 634
        if (!empty($geo)) {
            $this->element(
                'georss:point',
                null,
                $geo['coordinates'][0] . ' ' . $geo['coordinates'][1]
            );
635 636 637
        }
    }

638
    function showTwitterRssItem($entry)
639
    {
640 641 642 643 644 645
        $this->elementStart('item');
        $this->element('title', null, $entry['title']);
        $this->element('description', null, $entry['description']);
        $this->element('pubDate', null, $entry['pubDate']);
        $this->element('guid', null, $entry['guid']);
        $this->element('link', null, $entry['link']);
646 647

        # RSS only supports 1 enclosure per item
648
        if(array_key_exists('enclosures', $entry) and !empty($entry['enclosures'])){
649 650 651
            $enclosure = $entry['enclosures'][0];
            $this->element('enclosure', array('url'=>$enclosure['url'],'type'=>$enclosure['mimetype'],'length'=>$enclosure['size']), null);
        }
652

653
        if(array_key_exists('tags', $entry)){
654 655 656 657
            foreach($entry['tags'] as $tag){
                $this->element('category', null,$tag);
            }
        }
658

659
        $this->showGeoRSS($entry['geo']);
660
        $this->elementEnd('item');
661 662
    }

663
    function showJsonObjects($objects)
664
    {
665 666 667
        print(json_encode($objects));
    }

668
    function showSingleXmlStatus($notice)
669
    {
670 671 672 673
        $this->initDocument('xml');
        $twitter_status = $this->twitterStatusArray($notice);
        $this->showTwitterXmlStatus($twitter_status);
        $this->endDocument('xml');
674 675
    }

676 677
    function show_single_json_status($notice)
    {
678 679 680 681
        $this->initDocument('json');
        $status = $this->twitterStatusArray($notice);
        $this->showJsonObjects($status);
        $this->endDocument('json');
682 683
    }

684
    function showXmlTimeline($notice)
685
    {
686

687
        $this->initDocument('xml');
688
        $this->elementStart('statuses', array('type' => 'array'));
689 690 691

        if (is_array($notice)) {
            foreach ($notice as $n) {
692 693
                $twitter_status = $this->twitterStatusArray($n);
                $this->showTwitterXmlStatus($twitter_status);
694 695 696
            }
        } else {
            while ($notice->fetch()) {
697 698
                $twitter_status = $this->twitterStatusArray($notice);
                $this->showTwitterXmlStatus($twitter_status);
699 700 701
            }
        }

702
        $this->elementEnd('statuses');
703
        $this->endDocument('xml');
704 705
    }

706
    function showRssTimeline($notice, $title, $link, $subtitle, $suplink = null, $logo = null, $self = null)
707
    {
708

709
        $this->initDocument('rss');
710

711 712
        $this->element('title', null, $title);
        $this->element('link', null, $link);
713 714 715 716 717 718 719 720 721 722 723 724

        if (!is_null($self)) {
            $this->element(
                'atom:link',
                array(
                    'type' => 'application/rss+xml',
                    'href' => $self,
                    'rel'  => 'self'
                )
           );
        }

725
        if (!is_null($suplink)) {
726
            // For FriendFeed's SUP protocol
727
            $this->element('link', array('xmlns' => 'http://www.w3.org/2005/Atom',
728 729 730 731
                                         'rel' => 'http://api.friendfeed.com/2008/03#sup',
                                         'href' => $suplink,
                                         'type' => 'application/json'));
        }
732 733 734 735 736 737 738 739 740

        if (!is_null($logo)) {
            $this->elementStart('image');
            $this->element('link', null, $link);
            $this->element('title', null, $title);
            $this->element('url', null, $logo);
            $this->elementEnd('image');
        }

741 742 743
        $this->element('description', null, $subtitle);
        $this->element('language', null, 'en-us');
        $this->element('ttl', null, '40');
744 745 746

        if (is_array($notice)) {
            foreach ($notice as $n) {
747 748
                $entry = $this->twitterRssEntryArray($n);
                $this->showTwitterRssItem($entry);
749 750 751
            }
        } else {
            while ($notice->fetch()) {
752 753
                $entry = $this->twitterRssEntryArray($notice);
                $this->showTwitterRssItem($entry);
754 755 756
            }
        }

757
        $this->endTwitterRss();
758 759
    }

760
    function showAtomTimeline($notice, $title, $id, $link, $subtitle=null, $suplink=null, $selfuri=null, $logo=null)
761
    {
762

763
        $this->initDocument('atom');
764

765 766 767
        $this->element('title', null, $title);
        $this->element('id', null, $id);
        $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);
768

769 770 771 772
        if (!is_null($logo)) {
            $this->element('logo',null,$logo);
        }

773 774
        if (!is_null($suplink)) {
            # For FriendFeed's SUP protocol
775
            $this->element('link', array('rel' => 'http://api.friendfeed.com/2008/03#sup',
776 777 778
                                         'href' => $suplink,
                                         'type' => 'application/json'));
        }
779 780

        if (!is_null($selfuri)) {
781
            $this->element('link', array('href' => $selfuri,
782 783 784 785
                'rel' => 'self', 'type' => 'application/atom+xml'), null);
        }

        $this->element('updated', null, common_date_iso8601('now'));
786
        $this->element('subtitle', null, $subtitle);
787 788 789

        if (is_array($notice)) {
            foreach ($notice as $n) {
790
                $this->raw($n->asAtomEntry());
791 792 793
            }
        } else {
            while ($notice->fetch()) {
794
                $this->raw($notice->asAtomEntry());
795 796 797
            }
        }

798
        $this->endDocument('atom');
799 800 801

    }

802
    function showRssGroups($group, $title, $link, $subtitle)
803 804
    {

805
        $this->initDocument('rss');
806 807 808 809 810 811 812 813 814

        $this->element('title', null, $title);
        $this->element('link', null, $link);
        $this->element('description', null, $subtitle);
        $this->element('language', null, 'en-us');
        $this->element('ttl', null, '40');

        if (is_array($group)) {
            foreach ($group as $g) {
815 816
                $twitter_group = $this->twitterRssGroupArray($g);
                $this->showTwitterRssItem($twitter_group);
817 818 819
            }
        } else {
            while ($group->fetch()) {
820 821
                $twitter_group = $this->twitterRssGroupArray($group);
                $this->showTwitterRssItem($twitter_group);
822 823 824
            }
        }

825
        $this->endTwitterRss();
826 827
    }

828 829 830
    function showTwitterAtomEntry($entry)
    {
        $this->elementStart('entry');
831 832 833 834 835 836
        $this->element('title', null, common_xml_safe_str($entry['title']));
        $this->element(
            'content',
            array('type' => 'html'),
            common_xml_safe_str($entry['content'])
        );
837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861
        $this->element('id', null, $entry['id']);
        $this->element('published', null, $entry['published']);
        $this->element('updated', null, $entry['updated']);
        $this->element('link', array('type' => 'text/html',
                                     'href' => $entry['link'],
                                     'rel' => 'alternate'));
        $this->element('link', array('type' => $entry['avatar-type'],
                                     'href' => $entry['avatar'],
                                     'rel' => 'image'));
        $this->elementStart('author');

        $this->element('name', null, $entry['author-name']);
        $this->element('uri', null, $entry['author-uri']);

        $this->elementEnd('author');
        $this->elementEnd('entry');
    }

    function showXmlDirectMessage($dm)
    {
        $this->elementStart('direct_message');
        foreach($dm as $element => $value) {
            switch ($element) {
            case 'sender':
            case 'recipient':
862
                $this->showTwitterXmlUser($value, $element);
863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
                break;
            case 'text':
                $this->element($element, null, common_xml_safe_str($value));
                break;
            default:
                $this->element($element, null, $value);
                break;
            }
        }
        $this->elementEnd('direct_message');
    }

    function directMessageArray($message)
    {
        $dmsg = array();

        $from_profile = $message->getFrom();
        $to_profile = $message->getTo();

        $dmsg['id'] = $message->id;
        $dmsg['sender_id'] = $message->from_profile;
        $dmsg['text'] = trim($message->content);
        $dmsg['recipient_id'] = $message->to_profile;
886
        $dmsg['created_at'] = $this->dateTwitter($message->created);
887 888
        $dmsg['sender_screen_name'] = $from_profile->nickname;
        $dmsg['recipient_screen_name'] = $to_profile->nickname;
889 890
        $dmsg['sender'] = $this->twitterUserArray($from_profile, false);
        $dmsg['recipient'] = $this->twitterUserArray($to_profile, false);
891 892 893 894 895 896 897 898 899 900

        return $dmsg;
    }

    function rssDirectMessageArray($message)
    {
        $entry = array();

        $from = $message->getFrom();

901
        $entry['title'] = sprintf('Message from %1$s to %2$s',
902 903 904 905 906 907
            $from->nickname, $message->getTo()->nickname);

        $entry['content'] = common_xml_safe_str($message->rendered);
        $entry['link'] = common_local_url('showmessage', array('message' => $message->id));
        $entry['published'] = common_date_iso8601($message->created);

908
        $taguribase = TagURI::base();
909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931

        $entry['id'] = "tag:$taguribase:$entry[link]";
        $entry['updated'] = $entry['published'];

        $entry['author-name'] = $from->getBestName();
        $entry['author-uri'] = $from->homepage;

        $avatar = $from->getAvatar(AVATAR_STREAM_SIZE);

        $entry['avatar']      = (!empty($avatar)) ? $avatar->url : Avatar::defaultImage(AVATAR_STREAM_SIZE);
        $entry['avatar-type'] = (!empty($avatar)) ? $avatar->mediatype : 'image/png';

        // RSS item specific

        $entry['description'] = $entry['content'];
        $entry['pubDate'] = common_date_rfc2822($message->created);
        $entry['guid'] = $entry['link'];

        return $entry;
    }

    function showSingleXmlDirectMessage($message)
    {
932
        $this->initDocument('xml');
933 934
        $dmsg = $this->directMessageArray($message);
        $this->showXmlDirectMessage($dmsg);
935
        $this->endDocument('xml');
936 937 938 939
    }

    function showSingleJsonDirectMessage($message)
    {
940
        $this->initDocument('json');
941
        $dmsg = $this->directMessageArray($message);
942 943
        $this->showJsonObjects($dmsg);
        $this->endDocument('json');
944 945
    }

946
    function showAtomGroups($group, $title, $id, $link, $subtitle=null, $selfuri=null)
947 948
    {

949
        $this->initDocument('atom');
950

951
        $this->element('title', null, common_xml_safe_str($title));
952 953 954 955 956 957 958 959 960
        $this->element('id', null, $id);
        $this->element('link', array('href' => $link, 'rel' => 'alternate', 'type' => 'text/html'), null);

        if (!is_null($selfuri)) {
            $this->element('link', array('href' => $selfuri,
                'rel' => 'self', 'type' => 'application/atom+xml'), null);
        }

        $this->element('updated', null, common_date_iso8601('now'));
961
        $this->element('subtitle', null, common_xml_safe_str($subtitle));
962 963 964 965 966 967 968 969 970 971 972

        if (is_array($group)) {
            foreach ($group as $g) {
                $this->raw($g->asAtomEntry());
            }
        } else {
            while ($group->fetch()) {
                $this->raw($group->asAtomEntry());
            }
        }

973
        $this->endDocument('atom');