Twitter lists compatible people tags api

This commit is contained in:
Shashi Gowda 2011-03-06 23:36:38 +05:30
parent 4b8ee81ca9
commit 371e923c37
15 changed files with 2117 additions and 2 deletions

275
actions/apilist.php Normal file
View File

@ -0,0 +1,275 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Show, update or delete a list.
*
* 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 Shashi Gowda <connect2shashi@gmail.com>
* @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';
class ApiListAction extends ApiBareAuthAction
{
/**
* The list in question in the current request
*/
var $list = null;
/**
* Is this an update request?
*/
var $update = false;
/**
* Is this a delete request?
*/
var $delete = false;
/**
* Set the flags for handling the request. Show list if this is a GET
* request, update it if it is POST, delete list if method is DELETE
* or if method is POST and an argument _method is set to DELETE. Act
* like we don't know if the current user has no access to the list.
*
* Takes parameters:
* - user: the user id or nickname
* - id: the id of the tag or the tag itself
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
$this->delete = ($_SERVER['REQUEST_METHOD'] == 'DELETE' ||
($this->trimmed('_method') == 'DELETE' &&
$_SERVER['REQUEST_METHOD'] == 'POST'));
// update list if method is POST or PUT and $this->delete is not true
$this->update = (!$this->delete &&
in_array($_SERVER['REQUEST_METHOD'], array('POST', 'PUT')));
$this->user = $this->getTargetUser($this->arg('user'));
$this->list = $this->getTargetList($this->arg('user'), $this->arg('id'));
if (empty($this->list)) {
$this->clientError(_('Not found'), 404, $this->format);
return false;
}
return true;
}
/**
* Handle the request
*
* @return boolean success flag
*/
function handle($args)
{
parent::handle($args);
if($this->delete) {
$this->handleDelete();
return true;
}
if($this->update) {
$this->handlePut();
return true;
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($this->list);
break;
case 'json':
$this->showSingleJsonList($this->list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
/**
* require authentication if it is a write action or user is ambiguous
*
*/
function requiresAuth()
{
return parent::requiresAuth() ||
$this->create || $this->delete;
}
/**
* Update a list
*
* @return boolean success
*/
function handlePut()
{
if($this->auth_user->id != $this->list->tagger) {
$this->clientError(
_('You can not update lists that don\'t belong to you.'),
401,
$this->format
);
}
$new_list = clone($this->list);
$new_list->tag = common_canonical_tag($this->arg('name'));
$new_list->description = common_canonical_tag($this->arg('description'));
$new_list->private = ($this->arg('mode') === 'private') ? true : false;
$result = $new_list->update($this->list);
if(!$result) {
$this->clientError(
_('An error occured.'),
503,
$this->format
);
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($new_list);
break;
case 'json':
$this->showSingleJsonList($new_list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
/**
* Delete a list
*
* @return boolean success
*/
function handleDelete()
{
if($this->auth_user->id != $this->list->tagger) {
$this->clientError(
_('You can not delete lists that don\'t belong to you.'),
401,
$this->format
);
}
$record = clone($this->list);
$this->list->delete();
switch($this->format) {
case 'xml':
$this->showSingleXmlList($record);
break;
case 'json':
$this->showSingleJsonList($record);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
/**
* Indicate that this resource is not read-only.
*
* @return boolean is_read-only=false
*/
function isReadOnly($args)
{
return false;
}
/**
* When was the list (people tag) last updated?
*
* @return String time_last_modified
*/
function lastModified()
{
if(!empty($this->list)) {
return strtotime($this->list->modified);
}
return null;
}
/**
* An entity tag for this list
*
* Returns an Etag based on the action name, language, user ID and
* timestamps of the first and last list the user has joined
*
* @return string etag
*/
function etag()
{
if (!empty($this->list)) {
return '"' . implode(
':',
array($this->arg('action'),
common_language(),
$this->user->id,
strtotime($this->list->created),
strtotime($this->list->modified))
)
. '"';
}
return null;
}
}

124
actions/apilistmember.php Normal file
View File

@ -0,0 +1,124 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* API method to check if a user belongs to a list.
*
* 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 Shashi Gowda <connect2shashi@gmail.com>
* @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';
/**
* Action handler for Twitter list_memeber methods
*
* @category API
* @package StatusNet
* @author Shashi Gowda <connect2shashi@gmail.com>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @see ApiBareAuthAction
*/
class ApiListMemberAction extends ApiBareAuthAction
{
/**
* Set the flags for handling the request. Show the profile if this
* is a GET request AND the profile is a member of the list, add a member
* if it is a POST, remove the profile from the list if method is DELETE
* or if method is POST and an argument _method is set to DELETE. Act
* like we don't know if the current user has no access to the list.
*
* Takes parameters:
* - user: the user id or nickname
* - list_id: the id of the tag or the tag itself
* - id: the id of the member being looked for/added/removed
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
$this->user = $this->getTargetUser($this->arg('id'));
$this->list = $this->getTargetList($this->arg('user'), $this->arg('list_id'));
if (empty($this->list)) {
$this->clientError(_('Not found'), 404, $this->format);
return false;
}
if (empty($this->user)) {
$this->clientError(_('No such user'), 404, $this->format);
return false;
}
return true;
}
/**
* Handle the request
*
* @return boolean success flag
*/
function handle($args)
{
parent::handle($args);
$arr = array('tagger' => $this->list->tagger,
'tag' => $this->list->tag,
'tagged' => $this->user->id);
$ptag = Profile_tag::pkeyGet($arr);
if(empty($ptag)) {
$this->clientError(
_('The specified user is not a member of this list'),
400,
$this->format
);
}
$user = $this->twitterUserArray($this->user->getProfile(), true);
switch($this->format) {
case 'xml':
$this->showTwitterXmlUser($user, 'user', true);
break;
case 'json':
$this->showSingleJsonUser($user);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
return true;
}
}

173
actions/apilistmembers.php Normal file
View File

@ -0,0 +1,173 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* List/add/remove list members.
*
* 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 Shashi Gowda <connect2shashi@gmail.com>
* @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/apilistusers.php';
class ApiListMembersAction extends ApiListUsersAction
{
/**
* Add a user to a list (tag someone)
*
* @return boolean success
*/
function handlePost()
{
if($this->auth_user->id != $this->list->tagger) {
$this->clientError(
_('You aren\'t allowed to add members to this list'),
401,
$this->format
);
return false;
}
if($this->user === false) {
$this->clientError(
_('You must specify a member'),
400,
$this->format
);
return false;
}
$result = Profile_tag::setTag($this->auth_user->id,
$this->user->id, $this->list->tag);
if(empty($result)) {
$this->clientError(
_('An error occured.'),
500,
$this->format
);
return false;
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($this->list);
break;
case 'json':
$this->showSingleJsonList($this->list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
return false;
break;
}
}
/**
* Remove a user from a list (untag someone)
*
* @return boolean success
*/
function handleDelete()
{
if($this->auth_user->id != $this->list->tagger) {
$this->clientError(
_('You aren\'t allowed to remove members from this list'),
401,
$this->format
);
return false;
}
if($this->user === false) {
$this->clientError(
_('You must specify a member'),
400,
$this->format
);
return false;
}
$args = array('tagger' => $this->auth_user->id,
'tagged' => $this->user->id,
'tag' => $this->list->tag);
$ptag = Profile_tag::pkeyGet($args);
if(empty($ptag)) {
$this->clientError(
_('The user you are trying to remove from the list is not a member'),
400,
$this->format
);
return false;
}
$result = $ptag->delete();
if(empty($result)) {
$this->clientError(
_('An error occured.'),
500,
$this->format
);
return false;
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($this->list);
break;
case 'json':
$this->showSingleJsonList($this->list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
return false;
break;
}
return true;
}
/**
* List the members of a list (people tagged)
*/
function getUsers()
{
$fn = array($this->list, 'getTagged');
list($this->users, $this->next_cursor, $this->prev_cursor) =
Profile_list::getAtCursor($fn, array(), $this->cursor, 20);
}
}

View File

@ -0,0 +1,136 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Get a list of lists a user belongs to. (people tags for a user)
*
* 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 Shashi Gowda <connect2shashi@gmail.com>
* @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';
/**
* Action handler for API method to list lists a user belongs to.
* (people tags for a user)
*
* @category API
* @package StatusNet
* @author Shashi Gowda <connect2shashi@gmail.com>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @see ApiBareAuthAction
*/
class ApiListMembershipsAction extends ApiBareAuthAction
{
var $lists = array();
var $cursor = -1;
var $next_cursor = 0;
var $prev_cursor = 0;
/**
* Prepare for running the action
* Take arguments for running:s
*
* @param array $args $_REQUEST args
*
* @return boolean success flag
*
*/
function prepare($args)
{
parent::prepare($args);
$this->cursor = (int) $this->arg('cursor', -1);
$this->user = $this->getTargetUser($this->arg('user'));
if (empty($this->user)) {
$this->clientError(_('No such user.'), 404, $this->format);
return;
}
$this->getLists();
return true;
}
/**
* Handle the request
*
* Show the lists
*
* @param array $args $_REQUEST data (unused)
*
* @return void
*/
function handle($args)
{
parent::handle($args);
switch($this->format) {
case 'xml':
$this->showXmlLists($this->lists, $this->next_cursor, $this->prev_cursor);
break;
case 'json':
$this->showJsonLists($this->lists, $this->next_cursor, $this->prev_cursor);
break;
default:
$this->clientError(
_('API method not found.'),
400,
$this->format
);
break;
}
}
/**
* Return true if read only.
*
* MAY override
*
* @param array $args other arguments
*
* @return boolean is read only action?
*/
function isReadOnly($args)
{
return true;
}
function getLists()
{
$profile = $this->user->getProfile();
$fn = array($profile, 'getOtherTags');
# 20 lists
list($this->lists, $this->next_cursor, $this->prev_cursor) =
Profile_list::getAtCursor($fn, array($this->auth_user), $this->cursor, 20);
}
}

244
actions/apilists.php Normal file
View File

@ -0,0 +1,244 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* List existing lists or create a new list.
*
* 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 Shashi Gowda <connect2shashi@gmail.com>
* @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';
/**
* Action handler for Twitter list_memeber methods
*
* @category API
* @package StatusNet
* @author Shashi Gowda <connect2shashi@gmail.com>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @see ApiBareAuthAction
*/
class ApiListsAction extends ApiBareAuthAction
{
var $lists = null;
var $cursor = 0;
var $next_cursor = 0;
var $prev_cursor = 0;
var $create = false;
/**
* Set the flags for handling the request. List lists created by user if this
* is a GET request, create a new list if it is a POST request.
*
* Takes parameters:
* - user: the user id or nickname
* Parameters for POST request
* - name: name of the new list (the people tag itself)
* - mode: (optional) mode for the new list private/public
* - description: (optional) description for the list
*
* @return boolean success flag
*/
function prepare($args)
{
parent::prepare($args);
$this->create = ($_SERVER['REQUEST_METHOD'] == 'POST');
if (!$this->create) {
$this->user = $this->getTargetUser($this->arg('user'));
if (empty($this->user)) {
$this->clientError(_('No such user.'), 404, $this->format);
return false;
}
$this->getLists();
}
return true;
}
/**
* require authentication if it is a write action or user is ambiguous
*
*/
function requiresAuth()
{
return parent::requiresAuth() ||
$this->create || $this->delete;
}
/**
* Handle request:
* Show the lists the user has created if the request method is GET
* Create a new list by diferring to handlePost() if it is POST.
*/
function handle($args)
{
parent::handle($args);
if($this->create) {
return $this->handlePost();
}
switch($this->format) {
case 'xml':
$this->showXmlLists($this->lists, $this->next_cursor, $this->prev_cursor);
break;
case 'json':
$this->showJsonLists($this->lists, $this->next_cursor, $this->prev_cursor);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
/**
* Create a new list
*
* @return boolean success
*/
function handlePost()
{
$name=$this->arg('name');
if(empty($name)) {
// mimick twitter
print _("A list's name can't be blank.");
exit(1);
}
// twitter creates a new list by appending a number to the end
// if the list by the given name already exists
// it makes more sense to return the existing list instead
$private = null;
if ($this->arg('mode') === 'public') {
$private = false;
} else if ($this->arg('mode') === 'private') {
$private = true;
}
$list = Profile_list::ensureTag($this->auth_user->id,
$this->arg('name'),
$this->arg('description'),
$private);
if (empty($list)) {
return false;
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($list);
break;
case 'json':
$this->showSingleJsonList($list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
return true;
}
/**
* Get lists
*/
function getLists()
{
$cursor = (int) $this->arg('cursor', -1);
// twitter fixes count at 20
// there is no argument named count
$count = 20;
$profile = $this->user->getProfile();
$fn = array($profile, 'getOwnedTags');
list($this->lists,
$this->next_cursor,
$this->prev_cursor) = Profile_list::getAtCursor($fn, array($this->auth_user), $cursor, $count);
}
function isReadOnly($args)
{
return false;
}
function lastModified()
{
if (!$this->create && !empty($this->lists) && (count($this->lists) > 0)) {
return strtotime($this->lists[0]->created);
}
return null;
}
/**
* An entity tag for this list of lists
*
* Returns an Etag based on the action name, language, user ID and
* timestamps of the first and last list the user has joined
*
* @return string etag
*/
function etag()
{
if (!$this->create && !empty($this->lists) && (count($this->lists) > 0)) {
$last = count($this->lists) - 1;
return '"' . implode(
':',
array($this->arg('action'),
common_language(),
$this->user->id,
strtotime($this->lists[0]->created),
strtotime($this->lists[$last]->created))
)
. '"';
}
return null;
}
}

View File

@ -0,0 +1,91 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Check if a user is subscribed to a list
*
* 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
* @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';
class ApiListSubscriberAction extends ApiBareAuthAction
{
var $list = null;
function prepare($args)
{
parent::prepare($args);
$this->user = $this->getTargetUser($this->arg('id'));
$this->list = $this->getTargetList($this->arg('user'), $this->arg('list_id'));
if (empty($this->list)) {
$this->clientError(_('Not found'), 404, $this->format);
return false;
}
if (empty($this->user)) {
$this->clientError(_('No such user'), 404, $this->format);
return false;
}
return true;
}
function handle($args)
{
parent::handle($args);
$arr = array('profile_tag_id' => $this->list->id,
'profile_id' => $this->user->id);
$sub = Profile_tag_subscription::pkeyGet($arr);
if(empty($sub)) {
$this->clientError(
_('The specified user is not a subscriber of this list'),
400,
$this->format
);
}
$user = $this->twitterUserArray($this->user->getProfile(), true);
switch($this->format) {
case 'xml':
$this->showTwitterXmlUser($user, 'user', true);
break;
case 'json':
$this->showSingleJsonUser($user);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
}

View File

@ -0,0 +1,125 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Show/add/remove list 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
* @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/apilistusers.php';
class ApiListSubscribersAction extends ApiListUsersAction
{
/**
* Subscribe to list
*
* @return boolean success
*/
function handlePost()
{
$result = Profile_tag_subscription::add($this->list,
$this->auth_user);
if(empty($result)) {
$this->clientError(
_('An error occured.'),
500,
$this->format
);
return false;
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($this->list);
break;
case 'json':
$this->showSingleJsonList($this->list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
return false;
break;
}
}
function handleDelete()
{
$args = array('profile_tag_id' => $this->list->id,
'profile_id' => $this->auth_user->id);
$ptag = Profile_tag_subscription::pkeyGet($args);
if(empty($ptag)) {
$this->clientError(
_('You are not subscribed to this list'),
400,
$this->format
);
return false;
}
Profile_tag_subscription::remove($this->list, $this->auth_user);
if(empty($result)) {
$this->clientError(
_('An error occured.'),
500,
$this->format
);
return false;
}
switch($this->format) {
case 'xml':
$this->showSingleXmlList($this->list);
break;
case 'json':
$this->showSingleJsonList($this->list);
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
return false;
break;
}
return true;
}
function getUsers()
{
$fn = array($this->list, 'getSubscribers');
list($this->users, $this->next_cursor, $this->prev_cursor) =
Profile_list::getAtCursor($fn, array(), $this->cursor, 20);
}
}

View File

@ -0,0 +1,126 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Get a list of lists a user is subscribed to.
*
* 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
* @copyright 2009 StatusNet, Inc.
* @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @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';
class ApiListSubscriptionsAction extends ApiBareAuthAction
{
var $lists = array();
var $cursor = -1;
var $next_cursor = 0;
var $prev_cursor = 0;
/**
* Take arguments for running
*
* @param array $args $_REQUEST args
*
* @return boolean success flag
*
*/
function prepare($args)
{
parent::prepare($args);
$this->cursor = (int) $this->arg('cursor', -1);
$this->user = $this->getTargetUser($this->arg('user'));
$this->getLists();
return true;
}
/**
* Handle the request
*
* Show the lists
*
* @param array $args $_REQUEST data (unused)
*
* @return void
*/
function handle($args)
{
parent::handle($args);
if (empty($this->user)) {
$this->clientError(_('No such user.'), 404, $this->format);
return;
}
switch($this->format) {
case 'xml':
$this->showXmlLists($this->lists, $this->next_cursor, $this->prev_cursor);
break;
case 'json':
$this->showJsonLists($this->lists, $this->next_cursor, $this->prev_cursor);
break;
default:
$this->clientError(
_('API method not found.'),
400,
$this->format
);
break;
}
}
/**
* Return true if read only.
*
* MAY override
*
* @param array $args other arguments
*
* @return boolean is read only action?
*/
function isReadOnly($args)
{
return true;
}
function getLists()
{
if(empty($this->user)) {
return;
}
$profile = $this->user->getProfile();
$fn = array($profile, 'getTagSubscriptions');
# 20 lists
list($this->lists, $this->next_cursor, $this->prev_cursor) =
Profile_list::getAtCursor($fn, array(), $this->cursor, 20);
}
}

266
actions/apitimelinelist.php Normal file
View File

@ -0,0 +1,266 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Show a list's notices
*
* 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 Craig Andrews <candrews@integralblue.com>
* @author Evan Prodromou <evan@status.net>
* @author Jeffery To <jeffery.to@gmail.com>
* @author Zach Copley <zach@status.net>
* @copyright 2009 StatusNet, Inc.
* @copyright 2009 Free Software Foundation, Inc http://www.fsf.org
* @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/apiprivateauth.php';
require_once INSTALLDIR . '/lib/atomlistnoticefeed.php';
/**
* Returns the most recent notices (default 20) posted to the list specified by ID
*
* @category API
* @package StatusNet
* @author Craig Andrews <candrews@integralblue.com>
* @author Evan Prodromou <evan@status.net>
* @author Jeffery To <jeffery.to@gmail.com>
* @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 ApiTimelineListAction extends ApiPrivateAuthAction
{
var $list = null;
var $notices = array();
var $next_cursor = 0;
var $prev_cursor = 0;
var $cursor = -1;
/**
* Take arguments for running
*
* @param array $args $_REQUEST args
*
* @return boolean success flag
*
*/
function prepare($args)
{
parent::prepare($args);
$this->cursor = (int) $this->arg('cursor', -1);
$this->list = $this->getTargetList($this->arg('user'), $this->arg('id'));
return true;
}
/**
* Handle the request
*
* Just show the notices
*
* @param array $args $_REQUEST data (unused)
*
* @return void
*/
function handle($args)
{
parent::handle($args);
if (empty($this->list)) {
$this->clientError(_('List not found.'), 404, $this->format);
return false;
}
$this->getNotices();
$this->showTimeline();
}
/**
* Show the timeline of notices
*
* @return void
*/
function showTimeline()
{
// We'll pull common formatting out of this for other formats
$atom = new AtomListNoticeFeed($this->list, $this->auth_user);
$self = $this->getSelfUri();
switch($this->format) {
case 'xml':
$this->initDocument('xml');
$this->elementStart('statuses_list',
array('xmlns:statusnet' => 'http://status.net/schema/api/1/'));
$this->elementStart('statuses', array('type' => 'array'));
foreach ($this->notices as $n) {
$twitter_status = $this->twitterStatusArray($n);
$this->showTwitterXmlStatus($twitter_status);
}
$this->elementEnd('statuses');
$this->element('next_cursor', null, $this->next_cursor);
$this->element('previous_cursor', null, $this->prev_cursor);
$this->elementEnd('statuses_list');
$this->endDocument('xml');
break;
case 'rss':
$this->showRssTimeline(
$this->notices,
$atom->title,
$this->list->getUri(),
$atom->subtitle,
null,
$atom->logo,
$self
);
break;
case 'atom':
header('Content-Type: application/atom+xml; charset=utf-8');
try {
$atom->setId($self);
$atom->setSelfLink($self);
$atom->addEntryFromNotices($this->notices);
$this->raw($atom->getString());
} catch (Atom10FeedException $e) {
$this->serverError(
'Could not generate feed for list - ' . $e->getMessage()
);
return;
}
break;
case 'json':
$this->initDocument('json');
$statuses = array();
foreach ($this->notices as $n) {
$twitter_status = $this->twitterStatusArray($n);
array_push($statuses, $twitter_status);
}
$statuses_list = array('statuses' => $statuses,
'next_cursor' => $this->next_cusror,
'next_cursor_str' => strval($this->next_cusror),
'previous_cursor' => $this->prev_cusror,
'previous_cursor_str' => strval($this->prev_cusror)
);
$this->showJsonObjects($statuses_list);
$this->initDocument('json');
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
/**
* Get notices
*
* @return array notices
*/
function getNotices()
{
$fn = array($this->list, 'getNotices');
list($this->notices, $this->next_cursor, $this->prev_cursor) =
Profile_list::getAtCursor($fn, array(), $this->cursor, 20);
if (!$this->notices) {
$this->notices = array();
}
}
/**
* 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, list 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->list->id,
strtotime($this->notices[0]->created),
strtotime($this->notices[$last]->created))
)
. '"';
}
return null;
}
}

View File

@ -64,6 +64,7 @@ class ActivityObject
const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
const PERSON = 'http://activitystrea.ms/schema/1.0/person';
const GROUP = 'http://activitystrea.ms/schema/1.0/group';
const _LIST = 'http://activitystrea.ms/schema/1.0/list'; // LIST is reserved
const PLACE = 'http://activitystrea.ms/schema/1.0/place';
const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
// ^^^^^^^^^^ tea!
@ -92,6 +93,7 @@ class ActivityObject
public $title;
public $summary;
public $content;
public $owner;
public $link;
public $source;
public $avatarLinks = array();
@ -168,6 +170,10 @@ class ActivityObject
Activity::MEDIA
);
}
if ($this->type == self::_LIST) {
$owner = ActivityUtils::child($this->element, Activity::AUTHOR, Activity::SPEC);
$this->owner = new ActivityObject($owner);
}
}
private function _fromAuthor($element)
@ -520,13 +526,29 @@ class ActivityObject
AVATAR_MINI_SIZE);
$object->poco = PoCo::fromGroup($group);
Event::handle('EndActivityObjectFromGroup', array($group, &$object));
}
return $object;
}
static function fromPeopletag($ptag)
{
$object = new ActivityObject();
if (Event::handle('StartActivityObjectFromPeopletag', array($ptag, &$object))) {
$object->type = ActivityObject::_LIST;
$object->id = $ptag->getUri();
$object->title = $ptag->tag;
$object->summary = $ptag->description;
$object->link = $ptag->homeUrl();
$object->owner = Profile::staticGet('id', $ptag->tagger);
$object->poco = PoCo::fromProfile($object->owner);
Event::handle('EndActivityObjectFromPeopletag', array($ptag, &$object));
}
return $object;
}
function outputTo($xo, $tag='activity:object')
{
if (!empty($tag)) {
@ -601,6 +623,11 @@ class ActivityObject
}
}
if(!empty($this->owner)) {
$owner = $this->owner->asActivityNoun(self::AUTHOR);
$xo->raw($owner);
}
if (!empty($this->geopoint)) {
$xo->element(
'georss:point',

View File

@ -59,6 +59,7 @@ class ActivityVerb
const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow';
const LEAVE = 'http://ostatus.org/schema/1.0/leave';
const UNTAG = 'http://ostatus.org/schema/1.0/untag';
// For simple profile-update pings; no content to share.
const UPDATE_PROFILE = 'http://ostatus.org/schema/1.0/update-profile';

View File

@ -458,6 +458,32 @@ class ApiAction extends Action
return $entry;
}
function twitterListArray($list)
{
$profile = Profile::staticGet('id', $list->tagger);
$twitter_list = array();
$twitter_list['id'] = $list->id;
$twitter_list['name'] = $list->tag;
$twitter_list['full_name'] = '@'.$profile->nickname.'/'.$list->tag;;
$twitter_list['slug'] = $list->tag;
$twitter_list['description'] = $list->description;
$twitter_list['subscriber_count'] = $list->subscriberCount();
$twitter_list['member_count'] = $list->taggedCount();
$twitter_list['uri'] = $list->getUri();
if (isset($this->auth_user)) {
$twitter_list['following'] = $list->hasSubscriber($this->auth_user);
} else {
$twitter_list['following'] = false;
}
$twitter_list['mode'] = ($list->private) ? 'private' : 'public';
$twitter_list['user'] = $this->twitterUserArray($profile, false);
return $twitter_list;
}
function twitterRssEntryArray($notice)
{
$entry = array();
@ -633,6 +659,20 @@ class ApiAction extends Action
$this->elementEnd('group');
}
function showTwitterXmlList($twitter_list)
{
$this->elementStart('list');
foreach($twitter_list as $element => $value) {
if($element == 'user') {
$this->showTwitterXmlUser($value, 'user');
}
else {
$this->element($element, null, $value);
}
}
$this->elementEnd('list');
}
function showTwitterXmlUser($twitter_user, $role='user', $namespaces=false)
{
$attrs = array();
@ -1110,6 +1150,65 @@ class ApiAction extends Action
$this->endDocument('xml');
}
function showXmlLists($list, $next_cursor=0, $prev_cursor=0)
{
$this->initDocument('xml');
$this->elementStart('lists_list');
$this->elementStart('lists', array('type' => 'array'));
if (is_array($list)) {
foreach ($list as $l) {
$twitter_list = $this->twitterListArray($l);
$this->showTwitterXmlList($twitter_list);
}
} else {
while ($list->fetch()) {
$twitter_list = $this->twitterListArray($list);
$this->showTwitterXmlList($twitter_list);
}
}
$this->elementEnd('lists');
$this->element('next_cursor', null, $next_cursor);
$this->element('previous_cursor', null, $prev_cursor);
$this->elementEnd('lists_list');
$this->endDocument('xml');
}
function showJsonLists($list, $next_cursor=0, $prev_cursor=0)
{
$this->initDocument('json');
$lists = array();
if (is_array($list)) {
foreach ($list as $l) {
$twitter_list = $this->twitterListArray($l);
array_push($lists, $twitter_list);
}
} else {
while ($list->fetch()) {
$twitter_list = $this->twitterListArray($list);
array_push($lists, $twitter_list);
}
}
$lists_list = array(
'lists' => $lists,
'next_cursor' => $next_cursor,
'next_cursor_str' => strval($next_cursor),
'previous_cursor' => $prev_cursor,
'previous_cursor_str' => strval($prev_cursor)
);
$this->showJsonObjects($lists_list);
$this->endDocument('json');
}
function showTwitterXmlUsers($user)
{
$this->initDocument('xml');
@ -1171,6 +1270,22 @@ class ApiAction extends Action
$this->endDocument('xml');
}
function showSingleJsonList($list)
{
$this->initDocument('json');
$twitter_list = $this->twitterListArray($list);
$this->showJsonObjects($twitter_list);
$this->endDocument('json');
}
function showSingleXmlList($list)
{
$this->initDocument('xml');
$twitter_list = $this->twitterListArray($list);
$this->showTwitterXmlList($twitter_list);
$this->endDocument('xml');
}
function dateTwitter($dt)
{
$dateStr = date('d F Y H:i:s', strtotime($dt));
@ -1464,6 +1579,40 @@ class ApiAction extends Action
}
}
function getTargetList($user=null, $id=null)
{
$tagger = $this->getTargetUser($user);
$list = null;
if (empty($id)) {
$id = $this->arg('id');
}
if($id) {
if (is_numeric($id)) {
$list = Profile_list::staticGet('id', $id);
// only if the list with the id belongs to the tagger
if(empty($list) || $list->tagger != $tagger->id) {
$list = null;
}
}
if (empty($list)) {
$tag = common_canonical_tag($id);
$list = Profile_list::getByTaggerAndTag($tagger->id, $tag);
}
if (!empty($list) && $list->private) {
if ($this->auth_user->id == $list->tagger) {
return $list;
}
} else {
return $list;
}
}
return null;
}
/**
* Returns query argument or default value if not found. Certain
* parameters used throughout the API are lightly scrubbed and

207
lib/apilistusers.php Normal file
View File

@ -0,0 +1,207 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Base class for list members and list subscribers 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
* @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';
class ApiListUsersAction extends ApiBareAuthAction
{
var $list = null;
var $user = false;
var $create = false;
var $delete = false;
var $cursor = -1;
var $next_cursor = 0;
var $prev_cursor = 0;
var $users = null;
function prepare($args)
{
// delete list member if method is DELETE or if method is POST and an argument
// _method is set to DELETE
$this->delete = ($_SERVER['REQUEST_METHOD'] == 'DELETE' ||
($this->trimmed('_method') == 'DELETE' &&
$_SERVER['REQUEST_METHOD'] == 'POST'));
// add member if method is POST
$this->create = (!$this->delete &&
$_SERVER['REQUEST_METHOD'] == 'POST');
if($this->arg('id')) {
$this->user = $this->getTargetUser($this->arg('id'));
}
parent::prepare($args);
$this->list = $this->getTargetList($this->arg('user'), $this->arg('list_id'));
if (empty($this->list)) {
$this->clientError(_('Not found'), 404, $this->format);
return false;
}
if(!$this->create && !$this->delete) {
$this->getUsers();
}
return true;
}
function requiresAuth()
{
return parent::requiresAuth() ||
$this->create || $this->delete;
}
function handle($args)
{
parent::handle($args);
if($this->delete) {
return $this->handleDelete();
}
if($this->create) {
return $this->handlePost();
}
switch($this->format) {
case 'xml':
$this->initDocument('xml');
$this->elementStart('users_list', array('xmlns:statusnet' =>
'http://status.net/schema/api/1/'));
$this->elementStart('users', array('type' => 'array'));
if (is_array($this->users)) {
foreach ($this->users as $u) {
$twitter_user = $this->twitterUserArray($u, true);
$this->showTwitterXmlUser($twitter_user);
}
} else {
while ($this->users->fetch()) {
$twitter_user = $this->twitterUserArray($this->users, true);
$this->showTwitterXmlUser($twitter_user);
}
}
$this->elementEnd('users');
$this->element('next_cursor', null, $this->next_cursor);
$this->element('previous_cursor', null, $this->prev_cursor);
$this->elementEnd('users_list');
break;
case 'json':
$this->initDocument('json');
$users = array();
if (is_array($this->users)) {
foreach ($this->users as $u) {
$twitter_user = $this->twitterUserArray($u, true);
array_push($users, $twitter_user);
}
} else {
while ($this->users->fetch()) {
$twitter_user = $this->twitterUserArray($this->users, true);
array_push($users, $twitter_user);
}
}
$users_list = array('users' => $users,
'next_cursor' => $this->next_cursor,
'next_cursor_str' => strval($this->next_cursor),
'previous_cursor' => $this->prev_cursor,
'previous_cursor_str' => strval($this->prev_cursor));
$this->showJsonObjects($users_list);
$this->endDocument('json');
break;
default:
$this->clientError(
_('API method not found.'),
404,
$this->format
);
break;
}
}
function handlePost()
{
}
function handleDelete()
{
}
function getUsers()
{
}
function isReadOnly($args)
{
return false;
}
function lastModified()
{
if(!empty($this->list)) {
return strtotime($this->list->modified);
}
return null;
}
/**
* An entity tag for this list
*
* Returns an Etag based on the action name, language, user ID and
* timestamps of the first and last list the user has joined
*
* @return string etag
*/
function etag()
{
if (!empty($this->list)) {
return '"' . implode(
':',
array($this->arg('action'),
common_language(),
$this->list->id,
strtotime($this->list->created),
strtotime($this->list->modified))
)
. '"';
}
return null;
}
}

105
lib/atomlistnoticefeed.php Normal file
View File

@ -0,0 +1,105 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Class for building an in-memory Atom feed for a particular list'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 Feed
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @copyright 2010 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);
}
/**
* Class for list notice feeds. May contain a reference to the list.
*
* @category Feed
* @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 AtomListNoticeFeed extends AtomNoticeFeed
{
private $list;
private $tagger;
/**
* Constructor
*
* @param List $list the list for the feed
* @param User $cur the current authenticated user, if any
* @param boolean $indent flag to turn indenting on or off
*
* @return void
*/
function __construct($list, $cur = null, $indent = true) {
parent::__construct($cur, $indent);
$this->list = $list;
$this->tagger = Profile::staticGet('id', $list->tagger);
// TRANS: Title in atom list notice feed. %s is a list name.
$title = sprintf(_("Timeline for people tagged #%s by %s"), $list->tag, $this->tagger->nickname);
$this->setTitle($title);
$sitename = common_config('site', 'name');
$subtitle = sprintf(
// TRANS: Message is used as a subtitle in atom list notice feed.
// TRANS: %1$s is a list name, %2$s is a site name.
_('Updates from %1$s\'s %2$s people tag on %3$s!'),
$this->tagger->nickname,
$list->tag,
$sitename
);
$this->setSubtitle($subtitle);
$avatar = $this->tagger->avatarUrl(AVATAR_PROFILE_SIZE);
$this->setLogo($avatar);
$this->setUpdated('now');
$self = common_local_url('ApiTimelineList',
array('user' => $this->tagger->nickname,
'id' => $list->tag,
'format' => 'atom'));
$this->setId($self);
$this->setSelfLink($self);
// FIXME: Stop using activity:subject?
$ao = ActivityObject::fromPeopletag($this->list);
$this->addAuthorRaw($ao->asString('author').
$ao->asString('activity:subject'));
$this->addLink($this->list->getUri());
}
function getList()
{
return $this->list;
}
}

View File

@ -766,6 +766,72 @@ class Router
'id' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
// Lists (people tags)
$m->connect('api/lists/memberships.:format',
array('action' => 'ApiListMemberships',
'format' => '(xml|json)'));
$m->connect('api/:user/lists/memberships.:format',
array('action' => 'ApiListMemberships',
'user' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/lists/subscriptions.:format',
array('action' => 'ApiListSubscriptions',
'format' => '(xml|json)'));
$m->connect('api/:user/lists/subscriptions.:format',
array('action' => 'ApiListSubscriptions',
'user' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/lists.:format',
array('action' => 'ApiLists',
'format' => '(xml|json)'));
$m->connect('api/:user/lists.:format',
array('action' => 'ApiLists',
'user' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/:user/lists/:id.:format',
array('action' => 'ApiList',
'user' => '[a-zA-Z0-9]+',
'id' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/:user/lists/:id/statuses.:format',
array('action' => 'ApiTimelineList',
'user' => '[a-zA-Z0-9]+',
'id' => '[a-zA-Z0-9]+',
'format' => '(xml|json|rss|atom)'));
$m->connect('api/:user/:list_id/members.:format',
array('action' => 'ApiListMembers',
'user' => '[a-zA-Z0-9]+',
'list_id' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/:user/:list_id/subscribers.:format',
array('action' => 'ApiListSubscribers',
'user' => '[a-zA-Z0-9]+',
'list_id' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/:user/:list_id/members/:id.:format',
array('action' => 'ApiListMember',
'user' => '[a-zA-Z0-9]+',
'list_id' => '[a-zA-Z0-9]+',
'id' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
$m->connect('api/:user/:list_id/subscribers/:id.:format',
array('action' => 'ApiListSubscriber',
'user' => '[a-zA-Z0-9]+',
'list_id' => '[a-zA-Z0-9]+',
'id' => '[a-zA-Z0-9]+',
'format' => '(xml|json)'));
// Tags
$m->connect('api/statusnet/tags/timeline/:tag.:format',
array('action' => 'ApiTimelineTag',