]> git.mxchange.org Git - quix0rs-gnu-social.git/commitdiff
Merge branch '0.9.x' into refactor-api
authorZach Copley <zach@status.net>
Wed, 30 Sep 2009 17:32:05 +0000 (10:32 -0700)
committerZach Copley <zach@status.net>
Wed, 30 Sep 2009 17:32:05 +0000 (10:32 -0700)
* 0.9.x: (39 commits)
  Timeout a little incase the notice item from XHR response is
  Relocated the button for pop up window for notice stream
  Script no longer needed for Realtime plugin
  Better check to see if the XML prolog should be outputted for XML
  Outputting UTF-8 charset in document header irrespective of mimetype.
  Switched Doctype to XHTML 1.0 Strict (which best reflects the current
  Twitter API returns server errors in preferred format
  move HTTP error code strings to class variables
  remove string-checks from code using Notice::saveNew()
  change string return from Notice::saveNew to exceptions
  stop overwriting created timestamp on group edit
  Forgot to add home_timeline to the list of methods that only require
  Forgot to add home_timeline to the list of methods that only require
  moderator can delete another user's notice
  show delete button when user has deleteOthersNotice right
  let hooks override standard user rights
  user rights
  Merge DeleteAction class into DeletenoticeAction
  Fix some bugs in the URL linkification, and fixed the unit test.
  Fix URL linkification test cases for addition of 'title' attribution with long URL in f3c8fccc
  ...

actions/apifollowers.php [new file with mode: 0644]
actions/apifriends.php [new file with mode: 0644]
actions/apifriendstimeline.php [new file with mode: 0644]
actions/apimentions.php [new file with mode: 0644]
actions/apipublictimeline.php [new file with mode: 0644]
actions/apisubscriptions.php [new file with mode: 0644]
actions/apiusertimeline.php [new file with mode: 0644]
lib/apiauth.php [new file with mode: 0644]
lib/apibareauth.php [new file with mode: 0644]
lib/router.php
lib/twitterapi.php

diff --git a/actions/apifollowers.php b/actions/apifollowers.php
new file mode 100644 (file)
index 0000000..b216cce
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show a user's followers (subscribers)
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apibareauth.php';
+
+/**
+ * Ouputs the authenticating user's followers (subscribers), each with
+ * current Twitter-style status inline.  They are ordered by the order
+ * in which they subscribed to the user, 100 at a time.
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiFollowersAction extends ApiSubscriptionsAction
+{
+    /**
+     * Get the user's subscribers (followers) as an array of profiles
+     *
+     * @return array Profiles
+     */
+
+    function getProfiles()
+    {
+        $offset = ($this->page - 1) * $this->count;
+        $limit =  $this->count + 1;
+
+        $subs = null;
+
+        if (isset($this->tag)) {
+            $subs = $this->user->getTaggedSubscribers(
+                $this->tag, $offset, $limit
+            );
+        } else {
+            $subs = $this->user->getSubscribers(
+                $offset,
+                $limit
+            );
+        }
+
+        $profiles = array();
+
+        if (!empty($subs)) {
+            while ($subs->fetch()) {
+                $profiles[] = clone($subs);
+            }
+        }
+
+        return $profiles;
+    }
+
+}
diff --git a/actions/apifriends.php b/actions/apifriends.php
new file mode 100644 (file)
index 0000000..12751a6
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show a user's friends (subscriptions)
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apibareauth.php';
+
+/**
+ * Ouputs the authenticating user's friends (subscriptions), each with
+ * current Twitter-style status inline.  They are ordered by the date
+ * in which the user subscribed to them, 100 at a time.
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiFriendsAction extends ApiSubscriptionsAction
+{
+    /**
+     * Get the user's subscriptions (friends) as an array of profiles
+     *
+     * @return array Profiles
+     */
+
+    function getProfiles()
+    {
+        $offset = ($this->page - 1) * $this->count;
+        $limit =  $this->count + 1;
+
+        $subs = null;
+
+        if (isset($this->tag)) {
+            $subs = $this->user->getTaggedSubscriptions(
+                $this->tag, $offset, $limit
+            );
+        } else {
+            $subs = $this->user->getSubscriptions(
+                $offset,
+                $limit
+            );
+        }
+
+        $profiles = array();
+
+        if (!empty($subs)) {
+            while ($subs->fetch()) {
+                $profiles[] = clone($subs);
+            }
+        }
+
+        return $profiles;
+    }
+
+}
diff --git a/actions/apifriendstimeline.php b/actions/apifriendstimeline.php
new file mode 100644 (file)
index 0000000..1e88d1c
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show the friends timeline
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apibareauth.php';
+
+/**
+ * Returns the most recent notices (default 20) posted by the target user.
+ * This is the equivalent of 'You and friends' page accessed via Web.
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiFriendsTimelineAction extends ApiBareAuthAction
+{
+
+    var $user    = null;
+    var $notices = null;
+
+    /**
+     * Take arguments for running
+     *
+     * @param array $args $_REQUEST args
+     *
+     * @return boolean success flag
+     *
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $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);
+        $this->since    = $this->arg('since');
+
+        if ($this->requiresAuth()) {
+            if ($this->checkBasicAuthUser() == false) {
+                return;
+            }
+        }
+
+        $this->user = $this->getTargetUser($this->arg('id'));
+
+        if (empty($this->user)) {
+            $this->clientError(_('No such user!'), 404, $this->arg('format'));
+            return;
+        }
+
+        $this->notices = $this->getNotices();
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * Just show the notices
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+        $this->showTimeline();
+    }
+
+    /**
+     * Show the timeline of notices
+     *
+     * @return void
+     */
+
+    function showTimeline()
+    {
+        $profile    = $this->user->getProfile();
+        $sitename   = common_config('site', 'name');
+        $title      = sprintf(_("%s and friends"), $this->user->nickname);
+        $taguribase = common_config('integration', 'taguri');
+        $id         = "tag:$taguribase:FriendsTimeline:" . $this->user->id;
+        $link       = common_local_url(
+            'all', array('nickname' => $this->user->nickname)
+        );
+        $subtitle   = sprintf(
+            _('Updates from %1$s and friends on %2$s!'),
+            $this->user->nickname, $sitename
+        );
+
+        switch($this->arg('format')) {
+        case 'xml':
+            $this->show_xml_timeline($this->notices);
+            break;
+        case 'rss':
+            $this->show_rss_timeline($this->notices, $title, $link, $subtitle);
+            break;
+        case 'atom':
+
+            $target_id = $this->arg('id');
+
+            if (isset($target_id)) {
+                $selfuri = common_root_url() .
+                    'api/statuses/friends_timeline/' .
+                    $target_id . '.atom';
+            } else {
+                $selfuri = common_root_url() .
+                    'api/statuses/friends_timeline.atom';
+            }
+
+            $this->show_atom_timeline(
+                $this->notices, $title, $id, $link,
+                $subtitle, null, $selfuri
+            );
+            break;
+        case 'json':
+            $this->show_json_timeline($this->notices);
+            break;
+        default:
+            $this->clientError(_('API method not found!'), $code = 404);
+            break;
+        }
+    }
+
+    /**
+     * Get notices
+     *
+     * @return array notices
+     */
+
+    function getNotices()
+    {
+        $notices = array();
+
+        if (!empty($this->auth_user) && $this->auth_user->id == $this->user->id) {
+            $notice = $this->user->noticeInbox(
+                ($this->page-1) * $this->count,
+                $this->count, $this->since_id,
+                $this->max_id, $this->since
+            );
+        } else {
+            $notice = $this->user->noticesWithFriends(
+                ($this->page-1) * $this->count,
+                $this->count, $this->since_id,
+                $this->max_id, $this->since
+            );
+        }
+
+        while ($notice->fetch()) {
+            $notices[] = clone($notice);
+        }
+
+        return $notices;
+    }
+
+    /**
+     * Is this action read only?
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * When was this feed last modified?
+     *
+     * @return string datestamp of the latest notice in the stream
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+            return strtotime($this->notices[0]->created);
+        }
+
+        return null;
+    }
+
+    /**
+     * An entity tag for this stream
+     *
+     * Returns an Etag based on the action name, language, user ID, and
+     * timestamps of the first and last notice in the timeline
+     *
+     * @return string etag
+     */
+
+    function etag()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+
+            $last = count($this->notices) - 1;
+
+            return '"' . implode(
+                ':',
+                array($this->arg('action'),
+                      common_language(),
+                      $this->user->id,
+                      strtotime($this->notices[0]->created),
+                      strtotime($this->notices[$last]->created))
+            )
+            . '"';
+        }
+
+        return null;
+    }
+
+}
diff --git a/actions/apimentions.php b/actions/apimentions.php
new file mode 100644 (file)
index 0000000..43e93a9
--- /dev/null
@@ -0,0 +1,234 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show notices mentioning a user (@nickname)
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apibareauth.php';
+
+/**
+ * Returns the most recent (default 20) mentions (status containing @nickname)
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiMentionsAction extends ApiBareAuthAction
+{
+
+    var $user    = null;
+    var $notices = null;
+
+    /**
+     * Take arguments for running
+     *
+     * @param array $args $_REQUEST args
+     *
+     * @return boolean success flag
+     *
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $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);
+        $this->since    = $this->arg('since');
+
+        if ($this->requiresAuth()) {
+            if ($this->checkBasicAuthUser() == false) {
+                return;
+            }
+        }
+
+        $this->user = $this->getTargetUser($this->arg('id'));
+
+        if (empty($this->user)) {
+            $this->clientError(_('No such user!'), 404, $this->arg('format'));
+            return;
+        }
+
+        $this->notices = $this->getNotices();
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * Just show the notices
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+        $this->showTimeline();
+    }
+
+    /**
+     * Show the timeline of notices
+     *
+     * @return void
+     */
+
+    function showTimeline()
+    {
+        $profile = $this->user->getProfile();
+
+        $sitename   = common_config('site', 'name');
+        $title      = sprintf(
+            _('%1$s / Updates mentioning %2$s'),
+            $sitename, $this->user->nickname
+        );
+        $taguribase = common_config('integration', 'taguri');
+        $id         = "tag:$taguribase:Mentions:" . $this->user->id;
+        $link       = common_local_url(
+            'replies',
+            array('nickname' => $this->user->nickname)
+        );
+        $subtitle   = sprintf(
+            _('%1$s updates that reply to updates from %2$s / %3$s.'),
+            $sitename, $this->user->nickname, $profile->getBestName()
+        );
+
+        switch($this->arg('format')) {
+        case 'xml':
+            $this->show_xml_timeline($this->notices);
+            break;
+        case 'rss':
+            $this->show_rss_timeline($this->notices, $title, $link, $subtitle);
+            break;
+        case 'atom':
+            $selfuri = common_root_url() .
+                ltrim($_SERVER['QUERY_STRING'], 'p=');
+            $this->show_atom_timeline(
+                $this->notices, $title, $id, $link, $subtitle,
+                null, $selfuri
+            );
+            break;
+        case 'json':
+            $this->show_json_timeline($this->notices);
+            break;
+        default:
+            $this->clientError(_('API method not found!'), $code = 404);
+            break;
+        }
+    }
+
+    /**
+     * Get notices
+     *
+     * @return array notices
+     */
+
+    function getNotices()
+    {
+        $notices = array();
+
+        $notice = $this->user->getReplies(
+            ($this->page - 1) * $this->count, $this->count,
+            $this->since_id, $this->max_id, $this->since
+        );
+
+        while ($notice->fetch()) {
+            $notices[] = clone($notice);
+        }
+
+        return $notices;
+    }
+
+    /**
+     * Is this action read only?
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * When was this feed last modified?
+     *
+     * @return string datestamp of the latest notice in the stream
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+            return strtotime($this->notices[0]->created);
+        }
+
+        return null;
+    }
+
+    /**
+     * An entity tag for this stream
+     *
+     * Returns an Etag based on the action name, language, user ID, and
+     * timestamps of the first and last notice in the timeline
+     *
+     * @return string etag
+     */
+
+    function etag()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+
+            $last = count($this->notices) - 1;
+
+            return '"' . implode(
+                ':',
+                array($this->arg('action'),
+                      common_language(),
+                      $this->user->id,
+                      strtotime($this->notices[0]->created),
+                      strtotime($this->notices[$last]->created))
+            )
+            . '"';
+        }
+
+        return null;
+    }
+
+}
diff --git a/actions/apipublictimeline.php b/actions/apipublictimeline.php
new file mode 100644 (file)
index 0000000..2be979e
--- /dev/null
@@ -0,0 +1,207 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show the public timeline
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/twitterapi.php';
+
+/**
+ * Returns the most recent notices (default 20) posted by everybody
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiPublicTimelineAction extends TwitterapiAction
+{
+
+    var $notices = null;
+
+    /**
+     * Take arguments for running
+     *
+     * @param array $args $_REQUEST args
+     *
+     * @return boolean success flag
+     *
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $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);
+        $this->since    = $this->arg('since');
+
+        $this->notices = $this->getNotices();
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * Just show the notices
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+        $this->showTimeline();
+    }
+
+    /**
+     * Show the timeline of notices
+     *
+     * @return void
+     */
+
+    function showTimeline()
+    {
+        $sitename   = common_config('site', 'name');
+        $title      = sprintf(_("%s public timeline"), $sitename);
+        $taguribase = common_config('integration', 'taguri');
+        $id         = "tag:$taguribase:PublicTimeline";
+        $link       = common_root_url();
+        $subtitle   = sprintf(_("%s updates from everyone!"), $sitename);
+
+        switch($this->arg('format')) {
+        case 'xml':
+            $this->show_xml_timeline($this->notices);
+            break;
+        case 'rss':
+            $this->show_rss_timeline($this->notices, $title, $link, $subtitle);
+            break;
+        case 'atom':
+            $selfuri = common_root_url() . 'api/statuses/public_timeline.atom';
+            $this->show_atom_timeline(
+                $this->notices, $title, $id, $link,
+                $subtitle, null, $selfuri
+            );
+            break;
+        case 'json':
+            $this->show_json_timeline($this->notices);
+            break;
+        default:
+            $this->clientError(_('API method not found!'), $code = 404);
+            break;
+        }
+    }
+
+    /**
+     * Get notices
+     *
+     * @return array notices
+     */
+
+    function getNotices()
+    {
+        $notices = array();
+
+        $notice = Notice::publicStream(
+            ($this->page - 1) * $this->count, $this->count, $this->since_id,
+            $this->max_id, $this->since
+        );
+
+        while ($notice->fetch()) {
+            $notices[] = clone($notice);
+        }
+
+        return $notices;
+    }
+
+    /**
+     * Is this action read only?
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * When was this feed last modified?
+     *
+     * @return string datestamp of the latest notice in the stream
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+            return strtotime($this->notices[0]->created);
+        }
+
+        return null;
+    }
+
+    /**
+     * An entity tag for this stream
+     *
+     * Returns an Etag based on the action name, language, and
+     * timestamps of the first and last notice in the timeline
+     *
+     * @return string etag
+     */
+
+    function etag()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+
+            $last = count($this->notices) - 1;
+
+            return '"' . implode(
+                ':',
+                array($this->arg('action'),
+                      common_language(),
+                      strtotime($this->notices[0]->created),
+                      strtotime($this->notices[$last]->created))
+            )
+            . '"';
+        }
+
+        return null;
+    }
+
+}
diff --git a/actions/apisubscriptions.php b/actions/apisubscriptions.php
new file mode 100644 (file)
index 0000000..78dcd72
--- /dev/null
@@ -0,0 +1,275 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Base class for showing subscription information in the API
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apibareauth.php';
+
+/**
+ * This class outputs a list of profiles as Twitter-style user and status objects.
+ * It is used by the API methods /api/statuses/(friends|followers). To support the
+ * social graph methods it also can output a simple list of IDs.
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiSubscriptionsAction extends ApiBareAuthAction
+{
+
+    var $page     = null;
+    var $count    = null;
+    var $user     = null;
+    var $profiles = null;
+    var $format   = null;
+    var $tag      = null;
+    var $lite     = null;
+    var $ids_only = null;
+
+    /**
+     * Take arguments for running
+     *
+     * @param array $args $_REQUEST args
+     *
+     * @return boolean success flag
+     *
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $this->page     = (int)$this->arg('page', 1);
+        $this->tag      = $this->arg('tag');
+        $this->format   = $this->arg('format');
+
+        // Note: Twitter no longer supports 'lite'
+        $this->lite     = $this->arg('lite');
+
+        $this->ids_only = $this->arg('ids_only');
+
+        // If called as a social graph method, show 5000 per page, otherwise 100
+
+        $this->count    = isset($this->ids_only) ?
+            5000 : (int)$this->arg('count', 100);
+
+        if ($this->requiresAuth()) {
+            if ($this->checkBasicAuthUser() == false) {
+                return false;
+            }
+        }
+
+        $this->user = $this->getTargetUser($this->arg('id'));
+
+        if (empty($this->user)) {
+            $this->clientError(_('No such user!'), 404, $this->format);
+            return false;
+        }
+
+        $this->profiles = $this->getProfiles();
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * Show the profiles
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+
+        if (!in_array($this->format, array('xml', 'json'))) {
+            $this->clientError(_('API method not found!'), $code = 404);
+            return;
+        }
+
+        $this->init_document($this->format);
+
+        if (isset($this->ids_only)) {
+            $this->showIds();
+        } else {
+            $this->showProfiles(isset($this->lite) ? false : true);
+        }
+
+        $this->end_document($this->format);
+    }
+
+    /**
+     * Get profiles - should get overrrided
+     *
+     * @return array Profiles
+     */
+
+    function getProfiles()
+    {
+    }
+
+    /**
+     * Is this action read only?
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * When was this feed last modified?
+     *
+     * @return string datestamp of the latest profile in the stream
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->profiles) && (count($this->profiles) > 0)) {
+            return strtotime($this->profiles[0]->created);
+        }
+
+        return null;
+    }
+
+    /**
+     * An entity tag for this action
+     *
+     * Returns an Etag based on the action name, language, user ID, and
+     * timestamps of the first and last profiles in the subscriptions list
+     * There's also an indicator to show whether this action is being called
+     * as /api/statuses/(friends|followers) or /api/(friends|followers)/ids
+     *
+     * @return string etag
+     */
+
+    function etag()
+    {
+        if (!empty($this->profiles) && (count($this->profiles) > 0)) {
+
+            $last = count($this->profiles) - 1;
+
+            return '"' . implode(
+                ':',
+                array($this->arg('action'),
+                      common_language(),
+                      $this->user->id,
+                      isset($this->ids_only) ? 'IDs' : 'Profiles',
+                      strtotime($this->profiles[0]->created),
+                      strtotime($this->profiles[$last]->created))
+            )
+            . '"';
+        }
+
+        return null;
+    }
+
+    /**
+     * Show the profiles as Twitter-style useres and statuses
+     *
+     * @param boolean $include_statuses Whether to include the latest status
+     *                                  with each user. Default true.
+     *
+     * @return void
+     */
+
+    function showProfiles($include_statuses = true)
+    {
+        switch ($this->format) {
+        case 'xml':
+            $this->elementStart('users', array('type' => 'array'));
+            foreach ($this->profiles as $profile) {
+                $this->show_profile(
+                    $profile,
+                    $this->format,
+                    null,
+                    $include_statuses
+                );
+            }
+            $this->elementEnd('users');
+            break;
+        case 'json':
+            $arrays = array();
+            foreach ($this->profiles as $profile) {
+                $arrays[] = $this->twitter_user_array(
+                    $profile,
+                    $include_statuses
+                );
+            }
+            print json_encode($arrays);
+            break;
+        default:
+            $this->clientError(_('Unsupported format.'));
+            break;
+        }
+    }
+
+    /**
+     * Show the IDs of the profiles only. 5000 per page. To support
+     * the 'social graph' methods: /api/(friends|followers)/ids
+     *
+     * @return void
+     */
+
+    function showIds()
+    {
+        switch ($this->format) {
+        case 'xml':
+            $this->elementStart('ids');
+            foreach ($this->profiles as $profile) {
+                $this->element('id', null, $profile->id);
+            }
+            $this->elementEnd('ids');
+            break;
+        case 'json':
+            $ids = array();
+            foreach ($this->profiles as $profile) {
+                $ids[] = (int)$profile->id;
+            }
+            print json_encode($ids);
+            break;
+        default:
+            $this->clientError(_('Unsupported format.'));
+            break;
+        }
+    }
+
+}
diff --git a/actions/apiusertimeline.php b/actions/apiusertimeline.php
new file mode 100644 (file)
index 0000000..44d6941
--- /dev/null
@@ -0,0 +1,249 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Show a user's timeline
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apibareauth.php';
+
+/**
+ * Returns the most recent notices (default 20) posted by the authenticating
+ * user. Another user's timeline can be requested via the id parameter. This
+ * is the API equivalent of the user profile web page.
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiUserTimelineAction extends ApiBareAuthAction
+{
+
+    var $user    = null;
+    var $notices = null;
+
+    /**
+     * Take arguments for running
+     *
+     * @param array $args $_REQUEST args
+     *
+     * @return boolean success flag
+     *
+     */
+
+    function prepare($args)
+    {
+        parent::prepare($args);
+
+        $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);
+        $this->since    = $this->arg('since');
+
+        if ($this->requiresAuth()) {
+            if ($this->checkBasicAuthUser() == false) {
+                return;
+            }
+        }
+
+        $this->user = $this->getTargetUser($this->arg('id'));
+
+        if (empty($this->user)) {
+            $this->clientError(_('No such user!'), 404, $this->arg('format'));
+            return;
+        }
+
+        $this->notices = $this->getNotices();
+
+        return true;
+    }
+
+    /**
+     * Handle the request
+     *
+     * Just show the notices
+     *
+     * @param array $args $_REQUEST data (unused)
+     *
+     * @return void
+     */
+
+    function handle($args)
+    {
+        parent::handle($args);
+        $this->showTimeline();
+    }
+
+    /**
+     * Show the timeline of notices
+     *
+     * @return void
+     */
+
+    function showTimeline()
+    {
+        $profile = $this->user->getProfile();
+
+        $sitename   = common_config('site', 'name');
+        $title      = sprintf(_("%s timeline"), $this->user->nickname);
+        $taguribase = common_config('integration', 'taguri');
+        $id         = "tag:$taguribase:UserTimeline:" . $this->user->id;
+        $link       = common_local_url(
+            'showstream',
+            array('nickname' => $this->user->nickname)
+        );
+        $subtitle   = sprintf(
+            _('Updates from %1$s on %2$s!'),
+            $this->user->nickname, $sitename
+        );
+
+        // FriendFeed's SUP protocol
+        // Also added RSS and Atom feeds
+
+        $suplink = common_local_url('sup', null, null, $this->user->id);
+        header('X-SUP-ID: ' . $suplink);
+
+        switch($this->arg('format')) {
+        case 'xml':
+            $this->show_xml_timeline($this->notices);
+            break;
+        case 'rss':
+            $this->show_rss_timeline(
+                $this->notices, $title, $link,
+                $subtitle, $suplink
+            );
+            break;
+        case 'atom':
+            if (isset($apidata['api_arg'])) {
+                $selfuri = common_root_url() .
+                    'api/statuses/user_timeline/' .
+                    $apidata['api_arg'] . '.atom';
+            } else {
+                $selfuri = common_root_url() .
+                    'api/statuses/user_timeline.atom';
+            }
+            $this->show_atom_timeline(
+                $this->notices, $title, $id, $link,
+                $subtitle, $suplink, $selfuri
+            );
+            break;
+        case 'json':
+            $this->show_json_timeline($this->notices);
+            break;
+        default:
+            $this->clientError(_('API method not found!'), $code = 404);
+            break;
+        }
+
+    }
+
+    /**
+     * Get notices
+     *
+     * @return array notices
+     */
+
+    function getNotices()
+    {
+        $notices = array();
+
+        $notice = $this->user->getNotices(
+            ($this->page-1) * $this->count, $this->count,
+            $this->since_id, $this->max_id, $this->since
+        );
+
+        while ($notice->fetch()) {
+            $notices[] = clone($notice);
+        }
+
+        return $notices;
+    }
+
+    /**
+     * Is this action read only?
+     *
+     * @param array $args other arguments
+     *
+     * @return boolean true
+     */
+
+    function isReadOnly($args)
+    {
+        return true;
+    }
+
+    /**
+     * When was this feed last modified?
+     *
+     * @return string datestamp of the latest notice in the stream
+     */
+
+    function lastModified()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+            return strtotime($this->notices[0]->created);
+        }
+
+        return null;
+    }
+
+    /**
+     * An entity tag for this stream
+     *
+     * Returns an Etag based on the action name, language, user ID, and
+     * timestamps of the first and last notice in the timeline
+     *
+     * @return string etag
+     */
+
+    function etag()
+    {
+        if (!empty($this->notices) && (count($this->notices) > 0)) {
+
+            $last = count($this->notices) - 1;
+
+            return '"' . implode(
+                ':',
+                array($this->arg('action'),
+                      common_language(),
+                      $this->user->id,
+                      strtotime($this->notices[0]->created),
+                      strtotime($this->notices[$last]->created))
+            )
+            . '"';
+        }
+
+        return null;
+    }
+
+}
diff --git a/lib/apiauth.php b/lib/apiauth.php
new file mode 100644 (file)
index 0000000..f0b4b6b
--- /dev/null
@@ -0,0 +1,174 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Base class for API actions that require authentication
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/twitterapi.php';
+
+/**
+ * Actions extending this class will require auth
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiAuthAction extends TwitterapiAction
+{
+
+    var $auth_user = null;
+
+    /**
+     * Does this API resource require authentication?
+     *
+     * @return boolean true
+     */
+
+    function requiresAuth()
+    {
+        return true;
+    }
+
+    /**
+     * Check for a user specified via HTTP basic auth. If there isn't
+     * one, try to get one by outputting the basic auth header.
+     *
+     * @return boolean true or false
+     */
+
+    function checkBasicAuthUser()
+    {
+        $this->basicAuthProcessHeader();
+
+        if (!isset($this->auth_user)) {
+            header('WWW-Authenticate: Basic realm="StatusNet API"');
+
+            // show error if the user clicks 'cancel'
+
+            $this->showBasicAuthError();
+            return false;
+
+        } else {
+            $nickname = $this->auth_user;
+            $password = $this->auth_pw;
+            $this->auth_user = common_check_user($nickname, $password);
+
+            if (empty($this->auth_user)) {
+
+                // basic authentication failed
+
+                list($proxy, $ip) = common_client_ip();
+                common_log(
+                    LOG_WARNING,
+                    'Failed API auth attempt, nickname = ' .
+                    "$nickname, proxy = $proxy, ip = $ip."
+                );
+                $this->showBasicAuthError();
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Read the HTTP headers and set the auth user.  Decodes HTTP_AUTHORIZATION
+     * param to support basic auth when PHP is running in CGI mode.
+     *
+     * @return void
+     */
+
+    function basicAuthProcessHeader()
+    {
+        if (isset($_SERVER['AUTHORIZATION'])
+            || isset($_SERVER['HTTP_AUTHORIZATION'])
+        ) {
+                $authorization_header = isset($_SERVER['HTTP_AUTHORIZATION'])
+                ? $_SERVER['HTTP_AUTHORIZATION'] : $_SERVER['AUTHORIZATION'];
+        }
+
+        if (isset($_SERVER['PHP_AUTH_USER'])) {
+            $this->auth_user = $_SERVER['PHP_AUTH_USER'];
+            $this->auth_pw = $_SERVER['PHP_AUTH_PW'];
+        } elseif (isset($authorization_header)
+            && strstr(substr($authorization_header, 0, 5), 'Basic')) {
+
+            // decode the HTTP_AUTHORIZATION header on php-cgi server self
+            // on fcgid server the header name is AUTHORIZATION
+
+            $auth_hash = base64_decode(substr($authorization_header, 6));
+            list($this->auth_user, $this->auth_pw) = explode(':', $auth_hash);
+
+            // set all to null on a empty basic auth request
+
+            if ($this->auth_user == "") {
+                $this->auth_user = null;
+                $this->auth_pw = null;
+            }
+        } else {
+            $this->auth_user = null;
+            $this->auth_pw = null;
+        }
+    }
+
+    /**
+     * Output an authentication error message.  Use XML or JSON if one
+     * of those formats is specified, otherwise output plain text
+     *
+     * @return void
+     */
+
+    function showBasicAuthError()
+    {
+        header('HTTP/1.1 401 Unauthorized');
+        $msg = 'Could not authenticate you.';
+
+        if ($this->arg('format') == 'xml') {
+            header('Content-Type: application/xml; charset=utf-8');
+            $this->startXML();
+            $this->elementStart('hash');
+            $this->element('error', null, $msg);
+            $this->element('request', null, $_SERVER['REQUEST_URI']);
+            $this->elementEnd('hash');
+            $this->endXML();
+        } elseif ($this->arg('format') == 'json') {
+            header('Content-Type: application/json; charset=utf-8');
+            $error_array = array('error' => $msg,
+                                 'request' => $_SERVER['REQUEST_URI']);
+            print(json_encode($error_array));
+        } else {
+            header('Content-type: text/plain');
+            print "$msg\n";
+        }
+    }
+
+}
diff --git a/lib/apibareauth.php b/lib/apibareauth.php
new file mode 100644 (file)
index 0000000..a99d450
--- /dev/null
@@ -0,0 +1,79 @@
+<?php
+/**
+ * StatusNet, the distributed open-source microblogging tool
+ *
+ * Base class for API actions that require "bare auth". Bare auth means
+ * authentication is required only if the action is called without an argument
+ * or query param specifying user id.
+ *
+ * PHP version 5
+ *
+ * LICENCE: This program is free software: you can redistribute it and/or modify
+ * 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/>.
+ *
+ * @category  API
+ * @package   StatusNet
+ * @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/
+ */
+
+if (!defined('STATUSNET')) {
+    exit(1);
+}
+
+require_once INSTALLDIR.'/lib/apiauth.php';
+
+/**
+ * Actions extending this class will require auth unless a target
+ * user ID has been specified
+ *
+ * @category API
+ * @package  StatusNet
+ * @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 ApiBareAuthAction extends ApiAuthAction
+{
+    /**
+     * Does this API resource require authentication?
+     *
+     * @return boolean true or false
+     */
+
+    function requiresAuth()
+    {
+        // If the site is "private", all API methods except statusnet/config
+        // need authentication
+
+        if (common_config('site', 'private')) {
+            return true;
+        }
+
+        // check whether a user has been specified somehow
+
+        $id           = $this->arg('id');
+        $user_id      = $this->arg('user_id');
+        $screen_name  = $this->arg('screen_name');
+
+        if (empty($id) && empty($user_id) && empty($screen_name)) {
+            return true;
+        }
+
+        return false;
+    }
+
+}
\ No newline at end of file
index c18f273ed06ae6b948953d75ab0a392caad8d2f1..b3bb240d9a87f3c8c51f61ea11560f7df4e0cf57 100644 (file)
@@ -266,15 +266,81 @@ class Router
 
         // statuses API
 
+        $m->connect('api/statuses/public_timeline.:format',
+                    array('action' => 'ApiPublicTimeline',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/friends_timeline.:format',
+                    array('action' => 'ApiFriendsTimeline',
+                          'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/friends_timeline/:id.:format',
+                    array('action' => 'ApiFriendsTimeline',
+                          'id' => '[a-zA-Z0-9]+',
+                          'format' => '(xml|json|rss|atom)'));
+        $m->connect('api/statuses/home_timeline.:format',
+                    array('action' => 'ApiFriendsTimeline',
+                          'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/home_timeline/:id.:format',
+                    array('action' => 'ApiFriendsTimeline',
+                          'id' => '[a-zA-Z0-9]+',
+                          'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/user_timeline.:format',
+                    array('action' => 'ApiUserTimeline',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/user_timeline/:id.:format',
+                    array('action' => 'ApiUserTimeline',
+                    'id' => '[a-zA-Z0-9]+',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/mentions.:format',
+                    array('action' => 'ApiMentions',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/mentions/:id.:format',
+                    array('action' => 'ApiMentions',
+                    'id' => '[a-zA-Z0-9]+',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/replies.:format',
+                    array('action' => 'ApiMentions',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/replies/:id.:format',
+                    array('action' => 'ApiMentions',
+                    'id' => '[a-zA-Z0-9]+',
+                    'format' => '(xml|json|rss|atom)'));
+
+        $m->connect('api/statuses/friends.:format',
+                     array('action' => 'ApiFriends',
+                           'format' => '(xml|json)'));
+
+        $m->connect('api/statuses/friends/:id.:format',
+                    array('action' => 'ApiFriends',
+                    'id' => '[a-zA-Z0-9]+',
+                    'format' => '(xml|json)'));
+
+        $m->connect('api/statuses/followers.:format',
+                     array('action' => 'ApiFollowers',
+                           'format' => '(xml|json)'));
+
+        $m->connect('api/statuses/followers/:id.:format',
+                    array('action' => 'ApiFollowers',
+                    'id' => '[a-zA-Z0-9]+',
+                    'format' => '(xml|json)'));
+
         $m->connect('api/statuses/:method',
                     array('action' => 'api',
                           'apiaction' => 'statuses'),
-                    array('method' => '(public_timeline|home_timeline|friends_timeline|user_timeline|update|replies|mentions|show|friends|followers|featured)(\.(atom|rss|xml|json))?'));
+                    array('method' => '(update|show|featured)(\.(atom|rss|xml|json))?'));
 
         $m->connect('api/statuses/:method/:argument',
                     array('action' => 'api',
                           'apiaction' => 'statuses'),
-                    array('method' => '(user_timeline|home_timeline|friends_timeline|replies|mentions|show|destroy|friends|followers)'));
+                    array('method' => '(show|destroy)'));
 
         // users
 
@@ -329,29 +395,21 @@ class Router
 
         // Social graph
 
-        $m->connect('api/friends/ids/:argument',
-                    array('action' => 'api',
-                          'apiaction' => 'statuses',
-                          'method' => 'friendsIDs'));
+        $m->connect('api/friends/ids/:id.:format',
+                    array('action' => 'apiFriends',
+                          'ids_only' => true));
 
-        foreach (array('xml', 'json') as $e) {
-            $m->connect('api/friends/ids.'.$e,
-                        array('action' => 'api',
-                              'apiaction' => 'statuses',
-                              'method' => 'friendsIDs.'.$e));
-        }
+        $m->connect('api/followers/ids/:id.:format',
+                    array('action' => 'apiFollowers',
+                          'ids_only' => true));
 
-        $m->connect('api/followers/ids/:argument',
-                    array('action' => 'api',
-                          'apiaction' => 'statuses',
-                          'method' => 'followersIDs'));
+        $m->connect('api/friends/ids.:format',
+                    array('action' => 'apiFriends',
+                          'ids_only' => true));
 
-        foreach (array('xml', 'json') as $e) {
-            $m->connect('api/followers/ids.'.$e,
-                        array('action' => 'api',
-                              'apiaction' => 'statuses',
-                              'method' => 'followersIDs.'.$e));
-        }
+        $m->connect('api/followers/ids.:format',
+                     array('action' => 'apiFollowers',
+                          'ids_only' => true));
 
         // account
 
index 4a5de6ab3af99573badeac5441c88055db71580a..708738832baa20e3880e53c06170d10190b30c3b 100644 (file)
@@ -24,8 +24,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
 class TwitterapiAction extends Action
 {
 
-    var $auth_user;
-
     /**
      * Initialization.
      *
@@ -934,7 +932,7 @@ class TwitterapiAction extends Action
         return;
     }
 
-    function clientError($msg, $code = 400, $content_type = 'json')
+    function clientError($msg, $code = 400, $format = 'xml')
     {
         $action = $this->trimmed('action');
 
@@ -948,20 +946,23 @@ class TwitterapiAction extends Action
 
         header('HTTP/1.1 '.$code.' '.$status_string);
 
-        if ($content_type == 'xml') {
+        if ($format == 'xml') {
             $this->init_document('xml');
             $this->elementStart('hash');
             $this->element('error', null, $msg);
             $this->element('request', null, $_SERVER['REQUEST_URI']);
             $this->elementEnd('hash');
             $this->end_document('xml');
-        } else {
+        } elseif ($format == 'json'){
             $this->init_document('json');
             $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']);
             print(json_encode($error_array));
             $this->end_document('json');
-        }
+        } else {
 
+            // If user didn't request a useful format, throw a regular client error
+            throw new ClientException($msg, $code);
+        }
     }
 
     function serverError($msg, $code = 500, $content_type = 'json')
@@ -1073,6 +1074,38 @@ class TwitterapiAction extends Action
         }
     }
 
+    function getTargetUser($id)
+    {
+        if (empty($id)) {
+
+            // Twitter supports these other ways of passing the user ID
+            if (is_numeric($this->arg('id'))) {
+                return User::staticGet($this->arg('id'));
+            } else if ($this->arg('id')) {
+                $nickname = common_canonical_nickname($this->arg('id'));
+                return User::staticGet('nickname', $nickname);
+            } else if ($this->arg('user_id')) {
+                // This is to ensure that a non-numeric user_id still
+                // overrides screen_name even if it doesn't get used
+                if (is_numeric($this->arg('user_id'))) {
+                    return User::staticGet('id', $this->arg('user_id'));
+                }
+            } else if ($this->arg('screen_name')) {
+                $nickname = common_canonical_nickname($this->arg('screen_name'));
+                return User::staticGet('nickname', $nickname);
+            } else {
+                // Fall back to trying the currently authenticated user
+                return $this->auth_user;
+            }
+
+        } else if (is_numeric($id)) {
+            return User::staticGet($id);
+        } else {
+            $nickname = common_canonical_nickname($id);
+            return User::staticGet('nickname', $nickname);
+        }
+    }
+
     function get_group($id, $apidata=null)
     {
         if (empty($id)) {