Twitter-compatible API:

- Filled in favorites tags in statuses
  - Filled in more tags in user/show, including undocumented features
  - Better error handling and more consistent error messages
  - Code clean-up and refactoring
  - Removed huge obnoxious comments

darcs-hash:20081017023638-462f3-27b5d2709e423c616723d4bcfbc9d7d30a4ad161.gz
This commit is contained in:
zach 2008-10-16 22:36:38 -04:00
parent 9b75a8ea9f
commit b2f0ad1ccc
6 changed files with 125 additions and 299 deletions

View File

@ -40,20 +40,20 @@ class ApiAction extends Action {
$this->api_method = $method;
$this->content_type = strtolower($cmdext[1]);
} else {
#content type will be an extension on the method
# Requested format / content-type will be an extension on the method
$cmdext = explode('.', $method);
$this->api_method = $cmdext[0];
$this->content_type = strtolower($cmdext[1]);
}
# XXX Maybe check to see if the command actually exists first?
if($this->requires_auth()) {
if (!isset($_SERVER['PHP_AUTH_USER'])) {
# This header makes basic auth go
header('WWW-Authenticate: Basic realm="Laconica API"');
# if the user hits cancel -- bam!
# If the user hits cancel -- bam!
$this->show_basic_auth_error();
} else {
$nickname = $_SERVER['PHP_AUTH_USER'];
@ -69,6 +69,16 @@ class ApiAction extends Action {
}
}
} else {
# Caller might give us a username even if not required
if (isset($_SERVER['PHP_AUTH_USER'])) {
$user = User::staticGet('nickname', $_SERVER['PHP_AUTH_USER']);
if ($user) {
$this->user = $user;
}
# Twitter doesn't throw an error if the user isn't found
}
$this->process_command();
}
}
@ -76,19 +86,21 @@ class ApiAction extends Action {
function process_command() {
$action = "twitapi$this->api_action";
$actionfile = INSTALLDIR."/actions/$action.php";
if (file_exists($actionfile)) {
require_once($actionfile);
$action_class = ucfirst($action)."Action";
$action_obj = new $action_class();
if (method_exists($action_obj, $this->api_method)) {
$apidata = array( 'content-type' => $this->content_type,
'api_method' => $this->api_method,
'api_arg' => $this->api_arg,
'user' => $this->user);
call_user_func(array($action_obj, $this->api_method), $_REQUEST, $apidata);
} else {
common_user_error("API method not found!", $code=404);
}
} else {
common_user_error("API method not found!", $code=404);
@ -128,8 +140,24 @@ class ApiAction extends Action {
function show_basic_auth_error() {
header('HTTP/1.1 401 Unauthorized');
$msg = 'Could not authenticate you.';
if ($this->content_type == 'xml') {
header('Content-Type: application/xml; charset=utf-8');
common_start_xml();
common_element_start('hash');
common_element('error', NULL, $msg);
common_element('request', NULL, $_SERVER['REQUEST_URI']);
common_element_end('hash');
common_end_xml();
} else if ($this->content_type == '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("Could not authenticate you."); # exactly what Twitter says - no \n
print "$msg\n";
}
}
}

View File

@ -39,27 +39,12 @@ class TwitapifavoritesAction extends TwitterapiAction {
function favorites($args, $apidata) {
parent::handle($args);
$user = null;
// function was called with an argument /favorites/api_arg.format
if (isset($apidata['api_arg'])) {
if (is_numeric($apidata['api_arg'])) {
$user = User::staticGet($apidata['api_arg']);
} else {
$nickname = common_canonical_nickname($apidata['api_arg']);
$user = User::staticGet('nickname', $nickname);
}
} else {
// if no user was specified, then we'll use the authenticated user
$user = $apidata['user'];
}
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
if (!$user) {
// Set the user to be the auth user if asked-for can't be found
// honestly! This is what Twitter does, I swear --Zach
$user = $apidata['user'];
$this->client_error('Not Found', 404, $apidata['content-type']);
return;
}
$profile = $user->getProfile();
@ -116,11 +101,6 @@ class TwitapifavoritesAction extends TwitterapiAction {
function create($args, $apidata) {
parent::handle($args);
if (!in_array($apidata['content-type'], array('xml', 'json'))) {
common_user_error(_('API method not found!'), $code = 404);
return;
}
// Check for RESTfulness
if (!in_array($_SERVER['REQUEST_METHOD'], array('POST', 'DELETE'))) {
// XXX: Twitter just prints the err msg, no XML / JSON.
@ -128,7 +108,13 @@ class TwitapifavoritesAction extends TwitterapiAction {
return;
}
$user = $apidata['user'];
if (!in_array($apidata['content-type'], array('xml', 'json'))) {
common_user_error(_('API method not found!'), $code = 404);
return;
}
$this->auth_user = $apidata['user'];
$user = $this->auth_user;
$notice_id = $apidata['api_arg'];
$notice = Notice::staticGet($notice_id);

View File

@ -89,18 +89,6 @@ class TwitapifriendshipsAction extends TwitterapiAction {
}
//destroy
//
//Discontinues friendship with the user specified in the ID parameter as the authenticating user. Returns the un-friended user in the requested format when successful. Returns a string describing the failure condition when unsuccessful.
//
//URL: http://twitter.com/friendships/destroy/id.format
//
//Formats: xml, json
//
//Parameters:
//
//* id. Required. The ID or screen name of the user with whom to discontinue friendship. Ex: http://twitter.com/friendships/destroy/12345.json or http://twitter.com/friendships/destroy/bob.xml
function destroy($args, $apidata) {
parent::handle($args);
@ -136,19 +124,6 @@ class TwitapifriendshipsAction extends TwitterapiAction {
}
// Tests if a friendship exists between two users.
//
//
// URL: http://twitter.com/friendships/exists.format
//
// Formats: xml, json, none
//
// Parameters:
//
// * user_a. Required. The ID or screen_name of the first user to test friendship for.
// * user_b. Required. The ID or screen_name of the second user to test friendship for.
// * Ex: http://twitter.com/friendships/exists.xml?user_a=alice&user_b=bob
function exists($args, $apidata) {
parent::handle($args);

View File

@ -21,10 +21,6 @@ if (!defined('LACONICA')) { exit(1); }
require_once(INSTALLDIR.'/lib/twitterapi.php');
/* XXX: Please don't freak out about all the ugly comments in this file.
* They are mostly in here for reference while I work on the
* API. I'll fix things up later to make them look better later. -- Zach
*/
class TwitapistatusesAction extends TwitterapiAction {
function is_readonly() {
@ -100,26 +96,6 @@ class TwitapistatusesAction extends TwitterapiAction {
}
/*
Returns the 20 most recent statuses posted by the authenticating user and that user's friends.
This is the equivalent of /home on the Web.
URL: http://server/api/statuses/friends_timeline.format
Parameters:
* since. Optional. Narrows the returned results to just those statuses created after the specified
HTTP-formatted date. The same behavior is available by setting an If-Modified-Since header in
your HTTP request.
Ex: http://server/api/statuses/friends_timeline.rss?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
* since_id. Optional. Returns only statuses with an ID greater than (that is, more recent than)
the specified ID. Ex: http://server/api/statuses/friends_timeline.xml?since_id=12345
* count. Optional. Specifies the number of statuses to retrieve. May not be greater than 200.
Ex: http://server/api/statuses/friends_timeline.xml?count=5
* page. Optional. Ex: http://server/api/statuses/friends_timeline.rss?page=3
Formats: xml, json, rss, atom
*/
function friends_timeline($args, $apidata) {
parent::handle($args);
@ -141,19 +117,21 @@ class TwitapistatusesAction extends TwitterapiAction {
$since_id = 0;
}
// NOTE: before_id is an extensions to Twitter API -- TB
// NOTE: before_id is an extension to Twitter API -- TB
if (!$before_id) {
$before_id = 0;
}
$user = $this->get_user($id, $apidata);
$this->auth_user = $user;
$profile = $user->getProfile();
$sitename = common_config('site', 'name');
$siteserver = common_config('site', 'server');
$title = sprintf(_("%s and friends"), $user->nickname);
$id = "tag:$siteserver:friends:".$user->id;
$id = "tag:$siteserver:friends:" . $user->id;
$link = common_local_url('all', array('nickname' => $user->nickname));
$subtitle = sprintf(_('Updates from %1$s and friends on %2$s!'), $user->nickname, $sitename);
@ -178,55 +156,15 @@ class TwitapistatusesAction extends TwitterapiAction {
}
/*
Returns the 20 most recent statuses posted from the authenticating user. It's also possible to
request another user's timeline via the id parameter below. This is the equivalent of the Web
/archive page for your own user, or the profile page for a third party.
URL: http://server/api/statuses/user_timeline.format
Formats: xml, json, rss, atom
Parameters:
* id. Optional. Specifies the ID or screen name of the user for whom to return the
friends_timeline. Ex: http://server/api/statuses/user_timeline/12345.xml or
http://server/api/statuses/user_timeline/bob.json.
* count. Optional. Specifies the number of
statuses to retrieve. May not be greater than 200. Ex:
http://server/api/statuses/user_timeline.xml?count=5
* since. Optional. Narrows the returned
results to just those statuses created after the specified HTTP-formatted date. The same
behavior is available by setting an If-Modified-Since header in your HTTP request. Ex:
http://server/api/statuses/user_timeline.rss?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
* since_id. Optional. Returns only statuses with an ID greater than (that is, more recent than)
the specified ID. Ex: http://server/api/statuses/user_timeline.xml?since_id=12345 * page.
Optional. Ex: http://server/api/statuses/friends_timeline.rss?page=3
*/
function user_timeline($args, $apidata) {
parent::handle($args);
$user = null;
// function was called with an argument /statuses/user_timeline/api_arg.format
if (isset($apidata['api_arg'])) {
if (is_numeric($apidata['api_arg'])) {
$user = User::staticGet($apidata['api_arg']);
} else {
$nickname = common_canonical_nickname($apidata['api_arg']);
$user = User::staticGet('nickname', $nickname);
}
} else {
// if no user was specified, then we'll use the authenticated user
$user = $apidata['user'];
}
$this->auth_user = $apidata['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
if (!$user) {
// Set the user to be the auth user if asked-for can't be found
// honestly! This is what Twitter does, I swear --Zach
$user = $apidata['user'];
$this->client_error('Not Found', 404, $apidata['content-type']);
return;
}
$profile = $user->getProfile();
@ -304,7 +242,8 @@ class TwitapistatusesAction extends TwitterapiAction {
return;
}
$user = $apidata['user'];
$this->auth_user = $apidata['user'];
$user = $this->auth_user;
$status = $this->trimmed('status');
$source = $this->trimmed('source');
$in_reply_to_status_id = intval($this->trimmed('in_reply_to_status_id'));
@ -377,33 +316,18 @@ class TwitapistatusesAction extends TwitterapiAction {
$this->show($args, $apidata);
}
/*
Returns the 20 most recent @replies (status updates prefixed with @username) for the authenticating user.
URL: http://server/api/statuses/replies.format
Formats: xml, json, rss, atom
Parameters:
* page. Optional. Retrieves the 20 next most recent replies. Ex: http://server/api/statuses/replies.xml?page=3
* since. Optional. Narrows the returned results to just those replies created after the specified HTTP-formatted date. The
same behavior is available by setting an If-Modified-Since header in your HTTP request. Ex:
http://server/api/statuses/replies.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
* since_id. Optional. Returns only statuses with an ID greater than (that is, more recent than) the specified
ID. Ex: http://server/api/statuses/replies.xml?since_id=12345
*/
function replies($args, $apidata) {
parent::handle($args);
$since = $this->arg('since');
$count = $this->arg('count');
$page = $this->arg('page');
$since_id = $this->arg('since_id');
$before_id = $this->arg('before_id');
$user = $apidata['user'];
$this->auth_user = $apidata['user'];
$user = $this->auth_user;
$profile = $user->getProfile();
$sitename = common_config('site', 'name');
@ -426,7 +350,7 @@ class TwitapistatusesAction extends TwitterapiAction {
$since_id = 0;
}
// NOTE: before_id is an extensions to Twitter API -- TB
// NOTE: before_id is an extension to Twitter API -- TB
if (!$before_id) {
$before_id = 0;
}
@ -464,6 +388,7 @@ class TwitapistatusesAction extends TwitterapiAction {
return;
}
$this->auth_user = $apidata['user'];
$notice_id = $apidata['api_arg'];
$notice = Notice::staticGet($notice_id);
@ -480,22 +405,6 @@ class TwitapistatusesAction extends TwitterapiAction {
}
/*
Destroys the status specified by the required ID parameter. The authenticating user must be
the author of the specified status.
URL: http://server/api/statuses/destroy/id.format
Formats: xml, json
Parameters:
* id. Required. The ID of the status to destroy. Ex:
http://server/api/statuses/destroy/12345.json or
http://server/api/statuses/destroy/23456.xml
*/
function destroy($args, $apidata) {
parent::handle($args);
@ -512,7 +421,8 @@ class TwitapistatusesAction extends TwitterapiAction {
return;
}
$user = $apidata['user'];
$this->auth_user = $apidata['user'];
$user = $this->auth_user;
$notice_id = $apidata['api_arg'];
$notice = Notice::staticGet($notice_id);
@ -539,51 +449,11 @@ class TwitapistatusesAction extends TwitterapiAction {
}
# User Methods
/*
Returns up to 100 of the authenticating user's friends who have most recently updated, each with current status inline.
It's also possible to request another user's recent friends list via the id parameter below.
URL: http://server/api/statuses/friends.format
Formats: xml, json
Parameters:
* id. Optional. The ID or screen name of the user for whom to request a list of friends. Ex:
http://server/api/statuses/friends/12345.json
or
http://server/api/statuses/friends/bob.xml
* page. Optional. Retrieves the next 100 friends. Ex: http://server/api/statuses/friends.xml?page=2
* lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true. Ex:
http://server/api/statuses/friends.xml?lite=true
* since. Optional. Narrows the returned results to just those friendships created after the specified
HTTP-formatted date. The same behavior is available by setting an If-Modified-Since header in your HTTP
request. Ex: http://server/api/statuses/friends.xml?since=Tue%2C+27+Mar+2007+22%3A55%3A48+GMT
*/
function friends($args, $apidata) {
parent::handle($args);
return $this->subscriptions($apidata, 'subscribed', 'subscriber');
}
/*
Returns the authenticating user's followers, each with current status inline. They are ordered by the
order in which they joined Twitter (this is going to be changed).
URL: http://server/api/statuses/followers.format
Formats: xml, json
Parameters:
* id. Optional. The ID or screen name of the user for whom to request a list of followers. Ex:
http://server/api/statuses/followers/12345.json
or
http://server/api/statuses/followers/bob.xml
* page. Optional. Retrieves the next 100 followers. Ex: http://server/api/statuses/followers.xml?page=2
* lite. Optional. Prevents the inline inclusion of current status. Must be set to a value of true.
Ex: http://server/api/statuses/followers.xml?lite=true
*/
function followers($args, $apidata) {
parent::handle($args);
@ -592,11 +462,16 @@ class TwitapistatusesAction extends TwitterapiAction {
function subscriptions($apidata, $other_attr, $user_attr) {
$user = $this->get_subs_user($apidata);
# XXX: id
# XXX: lite
$this->auth_user = $apidate['user'];
$user = $this->get_user($apidata['api_arg'], $apidata);
if (!$user) {
$this->client_error('Not Found', 404, $apidata['content-type']);
return;
}
$page = $this->trimmed('page');
if (!$page || !is_numeric($page)) {
@ -632,32 +507,6 @@ class TwitapistatusesAction extends TwitterapiAction {
$this->end_document($type);
}
function get_subs_user($apidata) {
// function was called with an argument /statuses/user_timeline/api_arg.format
if (isset($apidata['api_arg'])) {
if (is_numeric($apidata['api_arg'])) {
$user = User::staticGet($apidata['api_arg']);
} else {
$nickname = common_canonical_nickname($apidata['api_arg']);
$user = User::staticGet('nickname', $nickname);
}
} else {
// if no user was specified, then we'll use the authenticated user
$user = $apidata['user'];
}
if (!$user) {
// Set the user to be the auth user if asked-for can't be found
// honestly! This is what Twitter does, I swear --Zach
$user = $apidata['user'];
}
return $user;
}
function show_profiles($profiles, $type) {
switch ($type) {
case 'xml':
@ -679,27 +528,11 @@ class TwitapistatusesAction extends TwitterapiAction {
}
}
/*
Returns a list of the users currently featured on the site with their current statuses inline.
URL: http://server/api/statuses/featured.format
Formats: xml, json
*/
function featured($args, $apidata) {
parent::handle($args);
common_server_error(_('API method under construction.'), $code=501);
}
function get_user($id, $apidata) {
if (!$id) {
return $apidata['user'];
} else if (is_numeric($id)) {
return User::staticGet($id);
} else {
return User::staticGet('nickname', $id);
}
}
function supported($cmd) {
$cmdlist = array('MessageCommand', 'SubCommand', 'UnsubCommand', 'FavCommand', 'OnCommand', 'OffCommand');
@ -712,4 +545,3 @@ class TwitapistatusesAction extends TwitterapiAction {
}
}

View File

@ -27,27 +27,6 @@ class TwitapiusersAction extends TwitterapiAction {
return true;
}
/*
Returns extended information of a given user, specified by ID or
screen name as per the required id parameter below. This information
includes design settings, so third party developers can theme their
widgets according to a given user's preferences. You must be properly
authenticated to request the page of a protected user.
URL: http://twitter.com/users/show/id.format
Formats: xml, json
Parameters:
* id. Required. The ID or screen name of a user.
Ex: http://twitter.com/users/show/12345.json or
http://twitter.com/users/show/bob.xml
* email. Optional. The email address of a user. Ex:
http://twitter.com/users/show.xml?email=test@example.com
*/
function show($args, $apidata) {
parent::handle($args);
@ -56,28 +35,19 @@ class TwitapiusersAction extends TwitterapiAction {
return;
}
$this->auth_user = $apidata['user'];
$user = null;
$email = $this->arg('email');
if (isset($apidata['api_arg'])) {
if (is_numeric($apidata['api_arg'])) {
// by user id
$user = User::staticGet($apidata['api_arg']);
} else {
// by nickname
$nickname = common_canonical_nickname($apidata['api_arg']);
$user = User::staticGet('nickname', $nickname);
}
} elseif ($email) {
// or, find user by email address
// XXX: The Twitter API spec say an id is *required*, but you can actually
// pull up a user with just an email address. -- Zach
if ($email) {
$user = User::staticGet('email', $email);
} elseif (isset($apidata['api_arg'])) {
$user = $this->get_user($apidata['api_arg']);
}
if (!$user) {
// XXX: Twitter returns a random(?) user instead of throwing and err! -- Zach
$this->client_error(_('User not found.'), 404, $apidata['content-type']);
$this->client_error(_('Not found.'), 404, $apidata['content-type']);
return;
}
@ -109,11 +79,34 @@ class TwitapiusersAction extends TwitterapiAction {
$twitter_user['profile_text_color'] = '';
$twitter_user['profile_link_color'] = '';
$twitter_user['profile_sidebar_fill_color'] = '';
$twitter_user['favourites_count'] = 0;
$twitter_user['utc_offset'] = '';
$twitter_user['time_zone'] = '';
$twitter_user['following'] = '';
$twitter_user['notifications'] = '';
$faves = DB_DataObject::factory('fave');
$faves->user_id = $user->id;
$faves_count = (int) $faves->count();
$twitter_user['favourites_count'] = $faves_count;
$timezone = 'UTC';
if ($user->timezone) {
$timezone = $user->timezone;
}
$t = new DateTime;
$t->setTimezone(new DateTimeZone($timezone));
$twitter_user['utc_offset'] = $t->format('Z');
$twitter_user['time_zone'] = $timezone;
if (isset($this->auth_user)) {
if ($this->auth_user->isSubscribed($profile)) {
$twitter_user['following'] = 'true';
} else {
$twitter_user['following'] = 'false';
}
// Not implemented yet
$twitter_user['notifications'] = 'false';
}
if ($apidata['content-type'] == 'xml') {
$this->init_document('xml');

View File

@ -21,6 +21,8 @@ if (!defined('LACONICA')) { exit(1); }
class TwitterapiAction extends Action {
var $auth_user;
function handle($args) {
parent::handle($args);
}
@ -53,10 +55,11 @@ class TwitterapiAction extends Action {
return $twitter_user;
}
function twitter_status_array($notice, $get_user=true) {
function twitter_status_array($notice, $include_user=true) {
$profile = $notice->getProfile();
$twitter_status = array();
$twitter_status['text'] = $notice->content;
$twitter_status['truncated'] = 'false'; # Not possible on Laconica
$twitter_status['created_at'] = $this->date_twitter($notice->created);
@ -64,12 +67,18 @@ class TwitterapiAction extends Action {
$twitter_status['source'] = $this->source_link($notice->source);
$twitter_status['id'] = intval($notice->id);
$twitter_status['in_reply_to_user_id'] = ($notice->reply_to) ? $this->replier_by_reply(intval($notice->reply_to)) : NULL;
$twitter_status['favorited'] = NULL; # XXX: Not implemented on Laconica yet.
if ($get_user) {
$profile = $notice->getProfile();
if (isset($this->auth_user)) {
common_debug("auth user set: " . $this->auth_user->nickname);
$twitter_status['favorited'] = ($this->auth_user->hasFave($notice)) ? 'true' : 'false';
} else {
common_debug("no auth user set");
$twitter_status['favorited'] = 'false';
}
if ($include_user) {
# Don't get notice (recursive!)
$twitter_user = $this->twitter_user_array($profile, false);
$twitter_user = $this->twitter_user_array($profile, false, $user);
$twitter_status['user'] = $twitter_user;
}
@ -505,11 +514,14 @@ class TwitterapiAction extends Action {
return;
}
function get_user($id) {
if (is_numeric($id)) {
function get_user($id, $apidata=NULL) {
if (!$id) {
return $apidata['user'];
} else if (is_numeric($id)) {
return User::staticGet($id);
} else {
return User::staticGet('nickname', $id);
$nickname = common_canonical_nickname($id);
return User::staticGet('nickname', $nickname);
}
}