From 371e923c37d947c8b276f555e612763f331dd31c Mon Sep 17 00:00:00 2001 From: Shashi Gowda Date: Sun, 6 Mar 2011 23:36:38 +0530 Subject: [PATCH] Twitter lists compatible people tags api --- actions/apilist.php | 275 +++++++++++++++++++++++++++++++ actions/apilistmember.php | 124 ++++++++++++++ actions/apilistmembers.php | 173 +++++++++++++++++++ actions/apilistmemberships.php | 136 +++++++++++++++ actions/apilists.php | 244 +++++++++++++++++++++++++++ actions/apilistsubscriber.php | 91 ++++++++++ actions/apilistsubscribers.php | 125 ++++++++++++++ actions/apilistsubscriptions.php | 126 ++++++++++++++ actions/apitimelinelist.php | 266 ++++++++++++++++++++++++++++++ lib/activityobject.php | 31 +++- lib/activityverb.php | 1 + lib/apiaction.php | 149 +++++++++++++++++ lib/apilistusers.php | 207 +++++++++++++++++++++++ lib/atomlistnoticefeed.php | 105 ++++++++++++ lib/router.php | 66 ++++++++ 15 files changed, 2117 insertions(+), 2 deletions(-) create mode 100644 actions/apilist.php create mode 100644 actions/apilistmember.php create mode 100644 actions/apilistmembers.php create mode 100644 actions/apilistmemberships.php create mode 100644 actions/apilists.php create mode 100644 actions/apilistsubscriber.php create mode 100644 actions/apilistsubscribers.php create mode 100644 actions/apilistsubscriptions.php create mode 100644 actions/apitimelinelist.php create mode 100644 lib/apilistusers.php create mode 100644 lib/atomlistnoticefeed.php diff --git a/actions/apilist.php b/actions/apilist.php new file mode 100644 index 0000000000..7a9ce70c5c --- /dev/null +++ b/actions/apilist.php @@ -0,0 +1,275 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @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; + } + +} diff --git a/actions/apilistmember.php b/actions/apilistmember.php new file mode 100644 index 0000000000..18d6ea370b --- /dev/null +++ b/actions/apilistmember.php @@ -0,0 +1,124 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @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 + * @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; + } +} diff --git a/actions/apilistmembers.php b/actions/apilistmembers.php new file mode 100644 index 0000000000..c6e92fa612 --- /dev/null +++ b/actions/apilistmembers.php @@ -0,0 +1,173 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @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); + } +} diff --git a/actions/apilistmemberships.php b/actions/apilistmemberships.php new file mode 100644 index 0000000000..635f970e87 --- /dev/null +++ b/actions/apilistmemberships.php @@ -0,0 +1,136 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @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 + * @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); + } +} diff --git a/actions/apilists.php b/actions/apilists.php new file mode 100644 index 0000000000..f520e32972 --- /dev/null +++ b/actions/apilists.php @@ -0,0 +1,244 @@ +. + * + * @category API + * @package StatusNet + * @author Shashi Gowda + * @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 + * @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; + } + +} diff --git a/actions/apilistsubscriber.php b/actions/apilistsubscriber.php new file mode 100644 index 0000000000..d6816b9b91 --- /dev/null +++ b/actions/apilistsubscriber.php @@ -0,0 +1,91 @@ +. + * + * @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; + } + } +} diff --git a/actions/apilistsubscribers.php b/actions/apilistsubscribers.php new file mode 100644 index 0000000000..e8468a195d --- /dev/null +++ b/actions/apilistsubscribers.php @@ -0,0 +1,125 @@ +. + * + * @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); + } +} diff --git a/actions/apilistsubscriptions.php b/actions/apilistsubscriptions.php new file mode 100644 index 0000000000..764360e149 --- /dev/null +++ b/actions/apilistsubscriptions.php @@ -0,0 +1,126 @@ +. + * + * @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); + } +} diff --git a/actions/apitimelinelist.php b/actions/apitimelinelist.php new file mode 100644 index 0000000000..f28eb59d53 --- /dev/null +++ b/actions/apitimelinelist.php @@ -0,0 +1,266 @@ +. + * + * @category API + * @package StatusNet + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @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 + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @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; + } + +} diff --git a/lib/activityobject.php b/lib/activityobject.php index a69e1a1b42..7771455443 100644 --- a/lib/activityobject.php +++ b/lib/activityobject.php @@ -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)); + 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', diff --git a/lib/activityverb.php b/lib/activityverb.php index 264351308b..5ee68f2880 100644 --- a/lib/activityverb.php +++ b/lib/activityverb.php @@ -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'; diff --git a/lib/apiaction.php b/lib/apiaction.php index ebda36db7f..a77d5da13f 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -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 diff --git a/lib/apilistusers.php b/lib/apilistusers.php new file mode 100644 index 0000000000..e4451c7c41 --- /dev/null +++ b/lib/apilistusers.php @@ -0,0 +1,207 @@ +. + * + * @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; + } + +} diff --git a/lib/atomlistnoticefeed.php b/lib/atomlistnoticefeed.php new file mode 100644 index 0000000000..fec7e16846 --- /dev/null +++ b/lib/atomlistnoticefeed.php @@ -0,0 +1,105 @@ +. + * + * @category Feed + * @package StatusNet + * @author Zach Copley + * @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 + * @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; + } + +} diff --git a/lib/router.php b/lib/router.php index ccc4b09781..ee1e4cd849 100644 --- a/lib/router.php +++ b/lib/router.php @@ -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',