From 34a6624452e8b7f60b40181441c6ea2c8158379a Mon Sep 17 00:00:00 2001 From: Mikael Nordfeldth Date: Sun, 6 Oct 2013 21:30:29 +0200 Subject: [PATCH] Qvitter API changes (thanks hannes2peer) I implemented changes from quitter.se's new API that their front-end qvitter uses, https://github.com/hannesmannerheim/qvitter/blob/master/api-changes-1.1.1/CHANGES However I left out the URL shortening commens, since I believe whatever behaviour they experienced that caused them to implement this was a bug (or many) and should be fixed in their proper areas and that shortening should not be entirely left out in API calls. --- actions/apiaccountregister.php | 252 ++++++++++++++++++ actions/apiaccountupdatebackgroundcolor.php | 103 +++++++ actions/apiaccountupdatelinkcolor.php | 104 ++++++++ actions/apiattachment.php | 101 +++++++ actions/apicheckhub.php | 117 ++++++++ actions/apichecknickname.php | 69 +++++ actions/apiconversation.php | 4 +- actions/apiexternalprofileshow.php | 95 +++++++ actions/apifriendshipsexists.php | 2 - actions/apigroupadmins.php | 191 +++++++++++++ actions/apigrouplistall.php | 2 - actions/apigroupmembership.php | 2 - actions/apigroupshow.php | 2 - actions/apihelptest.php | 2 - actions/apisearchatom.php | 2 - actions/apisearchjson.php | 24 +- actions/apistatusesfavs.php | 134 ++++++++++ actions/apistatusesshow.php | 2 - actions/apistatusnetversion.php | 2 - actions/apitimelinegroup.php | 2 - actions/apitimelinelist.php | 1 - actions/apitimelinepublic.php | 2 - actions/apitimelinetag.php | 6 +- actions/apitimelineuser.php | 3 +- actions/apitrends.php | 2 - actions/apiuserprofileimage.php | 2 - actions/apiusershow.php | 2 - classes/Notice.php | 13 +- classes/Profile.php | 9 +- classes/User_group.php | 7 +- lib/action.php | 2 +- lib/apiaction.php | 38 ++- ...ivateauth.php => apiprivateauthaction.php} | 0 lib/jsonsearchresultslist.php | 9 +- lib/noticelistitem.php | 2 +- lib/router.php | 36 +++ plugins/SubMirror/classes/SubMirror.php | 8 +- 37 files changed, 1274 insertions(+), 80 deletions(-) create mode 100644 actions/apiaccountregister.php create mode 100644 actions/apiaccountupdatebackgroundcolor.php create mode 100644 actions/apiaccountupdatelinkcolor.php create mode 100644 actions/apiattachment.php create mode 100644 actions/apicheckhub.php create mode 100644 actions/apichecknickname.php create mode 100644 actions/apiexternalprofileshow.php create mode 100644 actions/apigroupadmins.php create mode 100644 actions/apistatusesfavs.php rename lib/{apiprivateauth.php => apiprivateauthaction.php} (100%) diff --git a/actions/apiaccountregister.php b/actions/apiaccountregister.php new file mode 100644 index 0000000000..fec536a2c2 --- /dev/null +++ b/actions/apiaccountregister.php @@ -0,0 +1,252 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class ApiAccountRegisterAction extends ApiAction +{ + + /** + * Has there been an error? + */ + var $error = null; + + /** + * Have we registered? + */ + var $registered = false; + + /** + * Are we processing an invite? + */ + var $invite = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + $this->code = $this->trimmed('code'); + + if (empty($this->code)) { + common_ensure_session(); + if (array_key_exists('invitecode', $_SESSION)) { + $this->code = $_SESSION['invitecode']; + } + } + + if (common_config('site', 'inviteonly') && empty($this->code)) { + // TRANS: Client error displayed when trying to register to an invite-only site without an invitation. + $this->clientError(_('Sorry, only invited people can register.'),404,'json'); + return false; + } + + if (!empty($this->code)) { + $this->invite = Invitation::staticGet('code', $this->code); + if (empty($this->invite)) { + // TRANS: Client error displayed when trying to register to an invite-only site without a valid invitation. + $this->clientError(_('Sorry, invalid invitation code.'),404,'json'); + return false; + } + // Store this in case we need it + common_ensure_session(); + $_SESSION['invitecode'] = $this->code; + } + + return true; + } + + /** + * Handle the request + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError( + _('This method requires a POST.'), + 400, $this->format + ); + return; + + } else { + + $nickname = $this->trimmed('nickname'); + $email = $this->trimmed('email'); + $fullname = $this->trimmed('fullname'); + $homepage = $this->trimmed('homepage'); + $bio = $this->trimmed('bio'); + $location = $this->trimmed('location'); + + // We don't trim these... whitespace is OK in a password! + $password = $this->arg('password'); + $confirm = $this->arg('confirm'); + + // invitation code, if any + $code = $this->trimmed('code'); + + if ($code) { + $invite = Invitation::staticGet($code); + } + + if (common_config('site', 'inviteonly') && !($code && $invite)) { + // TRANS: Client error displayed when trying to register to an invite-only site without an invitation. + $this->clientError(_('Sorry, only invited people can register.'),404,'json'); + return; + } + + // Input scrubbing + try { + $nickname = Nickname::normalize($nickname); + } catch (NicknameException $e) { + $this->showForm($e->getMessage()); + } + $email = common_canonical_email($email); + + if ($email && !Validate::email($email, common_config('email', 'check_domain'))) { + // TRANS: Form validation error displayed when trying to register without a valid e-mail address. + $this->clientError(_('Not a valid email address.'),404,'json'); + } else if ($this->nicknameExists($nickname)) { + // TRANS: Form validation error displayed when trying to register with an existing nickname. + $this->clientError(_('Nickname already in use. Try another one.'),404,'json'); + } else if (!User::allowed_nickname($nickname)) { + // TRANS: Form validation error displayed when trying to register with an invalid nickname. + $this->clientError(_('Not a valid nickname.'),404,'json'); + } else if ($this->emailExists($email)) { + // TRANS: Form validation error displayed when trying to register with an already registered e-mail address. + $this->clientError(_('Email address already exists.'),404,'json'); + } else if (!is_null($homepage) && (strlen($homepage) > 0) && + !Validate::uri($homepage, + array('allowed_schemes' => + array('http', 'https')))) { + // TRANS: Form validation error displayed when trying to register with an invalid homepage URL. + $this->clientError(_('Homepage is not a valid URL.'),404,'json'); + return; + } else if (!is_null($fullname) && mb_strlen($fullname) > 255) { + // TRANS: Form validation error displayed when trying to register with a too long full name. + $this->clientError(_('Full name is too long (maximum 255 characters).'),404,'json'); + return; + } else if (Profile::bioTooLong($bio)) { + // TRANS: Form validation error on registration page when providing too long a bio text. + // TRANS: %d is the maximum number of characters for bio; used for plural. + $this->clientError(sprintf(_m('Bio is too long (maximum %d character).', + 'Bio is too long (maximum %d characters).', + Profile::maxBio()), + Profile::maxBio()),404,'json'); + return; + } else if (!is_null($location) && mb_strlen($location) > 255) { + // TRANS: Form validation error displayed when trying to register with a too long location. + $this->clientError(_('Location is too long (maximum 255 characters).'),404,'json'); + return; + } else if (strlen($password) < 6) { + // TRANS: Form validation error displayed when trying to register with too short a password. + $this->clientError(_('Password must be 6 or more characters.'),404,'json'); + return; + } else if ($password != $confirm) { + // TRANS: Form validation error displayed when trying to register with non-matching passwords. + $this->clientError(_('Passwords do not match.'),404,'json'); + } else { + + // annoy spammers + sleep(7); + + if ($user = User::register(array('nickname' => $nickname, + 'password' => $password, + 'email' => $email, + 'fullname' => $fullname, + 'homepage' => $homepage, + 'bio' => $bio, + 'location' => $location, + 'code' => $code))) { + if (!$user) { + // TRANS: Form validation error displayed when trying to register with an invalid username or password. + $this->clientError(_('Invalid username or password.'),404,'json'); + return; + } + + Event::handle('EndRegistrationTry', array($this)); + + $this->initDocument('json'); + $this->showJsonObjects($this->twitterUserArray($user->getProfile())); + $this->endDocument('json'); + + } else { + // TRANS: Form validation error displayed when trying to register with an invalid username or password. + $this->clientError(_('Invalid username or password.'),404,'json'); + } + } + } + } + + + /** + * Does the given nickname already exist? + * + * Checks a canonical nickname against the database. + * + * @param string $nickname nickname to check + * + * @return boolean true if the nickname already exists + */ + function nicknameExists($nickname) + { + $user = User::staticGet('nickname', $nickname); + return is_object($user); + } + + /** + * Does the given email address already exist? + * + * Checks a canonical email address against the database. + * + * @param string $email email address to check + * + * @return boolean true if the address already exists + */ + function emailExists($email) + { + $email = common_canonical_email($email); + if (!$email || strlen($email) == 0) { + return false; + } + $user = User::staticGet('email', $email); + return is_object($user); + } + +} diff --git a/actions/apiaccountupdatebackgroundcolor.php b/actions/apiaccountupdatebackgroundcolor.php new file mode 100644 index 0000000000..230ff5b585 --- /dev/null +++ b/actions/apiaccountupdatebackgroundcolor.php @@ -0,0 +1,103 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class ApiAccountUpdateBackgroundColorAction extends ApiAuthAction +{ + var $backgroundcolor = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = $this->auth_user; + + $this->backgroundcolor = $this->trimmed('backgroundcolor'); + return true; + } + + /** + * Handle the request + * + * Try to save the user's colors in her design. Create a new design + * if the user doesn't already have one. + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError( + _('This method requires a POST.'), + 400, $this->format + ); + return; + } + + $validhex = preg_match('/^[a-f0-9]{6}$/i',$this->backgroundcolor); + if($validhex === false || $validhex == 0) { + $this->clientError(_('Not a valid hex color.'),404,'json'); + return; + } + + // save the new color + $original = clone($this->user); + $this->user->backgroundcolor = $this->backgroundcolor; + if (!$this->user->update($original)) { + $this->clientError(_('Error updating user.'),404,'json'); + return; + } + + $profile = $this->user->getProfile(); + + if (empty($profile)) { + $this->clientError(_('User has no profile.'),'json'); + return; + } + + $twitter_user = $this->twitterUserArray($profile, true); + + $this->initDocument('json'); + $this->showJsonObjects($twitter_user); + $this->endDocument('json'); + } + + +} diff --git a/actions/apiaccountupdatelinkcolor.php b/actions/apiaccountupdatelinkcolor.php new file mode 100644 index 0000000000..9416a32cb7 --- /dev/null +++ b/actions/apiaccountupdatelinkcolor.php @@ -0,0 +1,104 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class ApiAccountUpdateLinkColorAction extends ApiAuthAction +{ + var $linkcolor = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->user = $this->auth_user; + + $this->linkcolor = $this->trimmed('linkcolor'); + + return true; + } + + /** + * Handle the request + * + * Try to save the user's colors in her design. Create a new design + * if the user doesn't already have one. + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if ($_SERVER['REQUEST_METHOD'] != 'POST') { + $this->clientError( + _('This method requires a POST.'), + 400, $this->format + ); + return; + } + + $validhex = preg_match('/^[a-f0-9]{6}$/i',$this->linkcolor); + if($validhex === false || $validhex == 0) { + $this->clientError(_('Not a valid hex color.'),404,'json'); + return; + } + + // save the new color + $original = clone($this->user); + $this->user->linkcolor = $this->linkcolor; + if (!$this->user->update($original)) { + $this->clientError(_('Error updating user.'),404,'json'); + return; + } + + $profile = $this->user->getProfile(); + + if (empty($profile)) { + $this->clientError(_('User has no profile.'),'json'); + return; + } + + $twitter_user = $this->twitterUserArray($profile, true); + + $this->initDocument('json'); + $this->showJsonObjects($twitter_user); + $this->endDocument('json'); + } + + +} diff --git a/actions/apiattachment.php b/actions/apiattachment.php new file mode 100644 index 0000000000..52cb570de3 --- /dev/null +++ b/actions/apiattachment.php @@ -0,0 +1,101 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Show a notice's attachment + * + */ +class ApiAttachmentAction extends ApiAuthAction +{ + const MAXCOUNT = 100; + + var $original = null; + var $cnt = self::MAXCOUNT; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + return true; + } + + /** + * Handle the request + * + * Make a new notice for the update, save it, and show it + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + $file = new File(); + $file->selectAdd(); // clears it + $file->selectAdd('url'); + $file->id = $this->trimmed('id'); + $url = $file->fetchAll('url'); + + $file_txt = ''; + if(strstr($url[0],'.html')) { + $file_txt['txt'] = file_get_contents(str_replace('://quitter.se','://127.0.0.1',$url[0])); + $file_txt['body_start'] = strpos($file_txt['txt'],'')+6; + $file_txt['body_end'] = strpos($file_txt['txt'],''); + $file_txt = substr($file_txt['txt'],$file_txt['body_start'],$file_txt['body_end']-$file_txt['body_start']); + } + + $this->initDocument('json'); + $this->showJsonObjects($file_txt); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return true; + } +} diff --git a/actions/apicheckhub.php b/actions/apicheckhub.php new file mode 100644 index 0000000000..d59506b667 --- /dev/null +++ b/actions/apicheckhub.php @@ -0,0 +1,117 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Check if a url have a push-hub, i.e. if it is possible to subscribe + * + */ +class ApiCheckHubAction extends ApiAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->url = urldecode($args['url']); + + if (!$this->url) { + $this->clientError(_('No URL.'), 403, 'json'); + return; + } + + if (!Validate::uri( + $this->url, array( + 'allowed_schemes' => + array('http', 'https') + ) + )) { + $this->clientError(_('Invalid URL.'), 403, 'json'); + return; + } + + return true; + } + + /** + * Handle the request + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + + $discover = new FeedDiscovery(); + + try { + $feeduri = $discover->discoverFromURL($this->url); + if($feeduri) { + $huburi = $discover->getHubLink(); + } + } catch (FeedSubNoFeedException $e) { + $this->clientError(_('No feed found'), 403, 'json'); + return; + } catch (FeedSubBadResponseException $e) { + $this->clientError(_('No hub found'), 403, 'json'); + return; + } + + $hub_status = array(); + if ($huburi) { + $hub_status = array('huburi' => $huburi); + } + + $this->initDocument('json'); + $this->showJsonObjects($hub_status); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return true; + } +} diff --git a/actions/apichecknickname.php b/actions/apichecknickname.php new file mode 100644 index 0000000000..7aa1283739 --- /dev/null +++ b/actions/apichecknickname.php @@ -0,0 +1,69 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +class ApiCheckNicknameAction extends ApiAction +{ + + function prepare($args) + { + parent::prepare($args); + + return true; + } + + function handle($args) + { + parent::handle($args); + + $nickname = $this->trimmed('nickname'); + + if ($this->nicknameExists($nickname)) { + $nickname_ok = 0; + } else if (!User::allowed_nickname($nickname)) { + $nickname_ok = 0; } + else { + $nickname_ok = 1; + } + + $this->initDocument('json'); + $this->showJsonObjects($nickname_ok); + $this->endDocument('json'); + } + + function nicknameExists($nickname) + { + $user = User::staticGet('nickname', $nickname); + return is_object($user); + } + +} diff --git a/actions/apiconversation.php b/actions/apiconversation.php index b3b44c7721..0e292303d5 100644 --- a/actions/apiconversation.php +++ b/actions/apiconversation.php @@ -75,9 +75,7 @@ class ApiconversationAction extends ApiAuthAction 404); } - $profile = Profile::current(); - - $stream = new ConversationNoticeStream($convId, $profile); + $stream = new ConversationNoticeStream($convId, $this->scoped); $notice = $stream->getNotices(($this->page-1) * $this->count, $this->count, diff --git a/actions/apiexternalprofileshow.php b/actions/apiexternalprofileshow.php new file mode 100644 index 0000000000..2acc97f48f --- /dev/null +++ b/actions/apiexternalprofileshow.php @@ -0,0 +1,95 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Ouputs information for a user, specified by ID or screen name. + * The user's most recent status will be returned inline. + */ +class ApiExternalProfileShowAction extends ApiPrivateAuthAction +{ + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + * + */ + function prepare($args) + { + parent::prepare($args); + + $profileurl = urldecode($this->arg('profileurl')); + + $this->profile = Profile::staticGet('profileurl', $profileurl); + + return true; + } + + /** + * Handle the request + * + * Check the format and show the user info + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + if (empty($this->profile)) { + // TRANS: Client error displayed when requesting profile information for a non-existing profile. + $this->clientError(_('Profile not found.'), 404, 'json'); + return; + } + + $twitter_user = $this->twitterUserArray($this->profile, true); + + $this->initDocument('json'); + $this->showJsonObjects($twitter_user); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + function isReadOnly($args) + { + return true; + } +} diff --git a/actions/apifriendshipsexists.php b/actions/apifriendshipsexists.php index 43b1daf4fc..1f76e00b1a 100644 --- a/actions/apifriendshipsexists.php +++ b/actions/apifriendshipsexists.php @@ -33,8 +33,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Tests for the existence of friendship between two users. Will return true if * user_a follows user_b, otherwise will return false. diff --git a/actions/apigroupadmins.php b/actions/apigroupadmins.php new file mode 100644 index 0000000000..3ddff15480 --- /dev/null +++ b/actions/apigroupadmins.php @@ -0,0 +1,191 @@ +. + * + * @category API + * @package GNUSocial + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @author Hannes Mannerheim + * @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://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { + exit(1); +} + +/** + * List 20 newest admins of the group specified by name or ID. + * + * @category API + * @package GNUSocial + * @author Craig Andrews + * @author Evan Prodromou + * @author Jeffery To + * @author Zach Copley + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ +class ApiGroupAdminsAction extends ApiPrivateAuthAction +{ + var $group = null; + var $profiles = null; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $this->group = $this->getTargetGroup($this->arg('id')); + if (empty($this->group)) { + // TRANS: Client error displayed trying to show group membership on a non-existing group. + $this->clientError(_('Group not found.'), 404, $this->format); + return false; + } + + $this->profiles = $this->getProfiles(); + + return true; + } + + /** + * Handle the request + * + * Show the admin of the group + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + // XXX: RSS and Atom + + switch($this->format) { + case 'xml': + $this->showTwitterXmlUsers($this->profiles); + break; + case 'json': + $this->showJsonUsers($this->profiles); + break; + default: + $this->clientError( + // TRANS: Client error displayed when coming across a non-supported API method. + _('API method not found.'), + 404, + $this->format + ); + break; + } + } + + /** + * Fetch the admins of a group + * + * @return array $profiles list of profiles + */ + function getProfiles() + { + $profiles = array(); + + $profile = $this->group->getAdmins( + ($this->page - 1) * $this->count, + $this->count, + $this->since_id, + $this->max_id + ); + + while ($profile->fetch()) { + $profiles[] = clone($profile); + } + + return $profiles; + } + + /** + * Is this action read only? + * + * @param array $args other arguments + * + * @return boolean true + */ + function isReadOnly($args) + { + return true; + } + + /** + * When was this list of profiles last modified? + * + * @return string datestamp of the lastest profile in the group + */ + function lastModified() + { + if (!empty($this->profiles) && (count($this->profiles) > 0)) { + return strtotime($this->profiles[0]->created); + } + + return null; + } + + /** + * An entity tag for this list of groups + * + * Returns an Etag based on the action name, language + * the group id, and timestamps of the first and last + * user who has joined the group + * + * @return string etag + */ + function etag() + { + if (!empty($this->profiles) && (count($this->profiles) > 0)) { + + $last = count($this->profiles) - 1; + + return '"' . implode( + ':', + array($this->arg('action'), + common_user_cache_hash($this->auth_user), + common_language(), + $this->group->id, + strtotime($this->profiles[0]->created), + strtotime($this->profiles[$last]->created)) + ) + . '"'; + } + + return null; + } +} diff --git a/actions/apigrouplistall.php b/actions/apigrouplistall.php index 51c3df1b2f..2fb3714257 100644 --- a/actions/apigrouplistall.php +++ b/actions/apigrouplistall.php @@ -35,8 +35,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns of the lastest 20 groups for the site * diff --git a/actions/apigroupmembership.php b/actions/apigroupmembership.php index 7ad8fb767e..ed78d9eda8 100644 --- a/actions/apigroupmembership.php +++ b/actions/apigroupmembership.php @@ -35,8 +35,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * List 20 newest members of the group specified by name or ID. * diff --git a/actions/apigroupshow.php b/actions/apigroupshow.php index e99777e32c..15b9edb975 100644 --- a/actions/apigroupshow.php +++ b/actions/apigroupshow.php @@ -35,8 +35,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Outputs detailed information about the group specified by ID * diff --git a/actions/apihelptest.php b/actions/apihelptest.php index 1bbbe572bf..a9cd7394c9 100644 --- a/actions/apihelptest.php +++ b/actions/apihelptest.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns the string "ok" in the requested format with a 200 OK HTTP status code. * diff --git a/actions/apisearchatom.php b/actions/apisearchatom.php index 075a4df83d..fdf95f1ce9 100644 --- a/actions/apisearchatom.php +++ b/actions/apisearchatom.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/apiprivateauth.php'; - /** * Action for outputting search results in Twitter compatible Atom * format. diff --git a/actions/apisearchjson.php b/actions/apisearchjson.php index 710ccdcccf..9f1f71d355 100644 --- a/actions/apisearchjson.php +++ b/actions/apisearchjson.php @@ -20,19 +20,15 @@ * along with this program. If not, see . * * @category Search - * @package StatusNet + * @package GNUSocial * @author Zach Copley * @copyright 2008-2010 StatusNet, Inc. + * @copyright 2013 Free Software Foundation, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @link http://status.net/ + * @link http://www.gnu.org/software/social/ */ -if (!defined('STATUSNET') && !defined('LACONICA')) { - exit(1); -} - -require_once INSTALLDIR.'/lib/apiprivateauth.php'; -require_once INSTALLDIR.'/lib/jsonsearchresultslist.php'; +if (!defined('GNUSOCIAL')) { exit(1); } /** * Action handler for Twitter-compatible API search @@ -89,12 +85,6 @@ class ApiSearchJSONAction extends ApiPrivateAuthAction $this->since_id = $this->trimmed('since_id'); $this->geocode = $this->trimmed('geocode'); - if (!empty($this->auth_user)) { - $this->auth_profile = $this->auth_user->getProfile(); - } else { - $this->auth_profile = null; - } - return true; } @@ -123,15 +113,15 @@ class ApiSearchJSONAction extends ApiPrivateAuthAction // TODO: Support search operators like from: and to:, boolean, etc. if (preg_match('/^#([\pL\pN_\-\.]{1,64})$/ue', $q)) { - $stream = new TagNoticeStream(substr($q, 1), $this->auth_profile); + $stream = new TagNoticeStream(substr($q, 1), $this->scoped); } else if ($this->isAnURL($q)) { $canon = File_redirection::_canonUrl($q); $file = File::getKV('url', $canon); if (!empty($file)) { - $stream = new FileNoticeStream($file, $this->auth_profile); + $stream = new FileNoticeStream($file, $this->scoped); } } else { - $stream = new SearchNoticeStream($q, $this->auth_profile); + $stream = new SearchNoticeStream($q, $this->scoped); } if (empty($stream)) { diff --git a/actions/apistatusesfavs.php b/actions/apistatusesfavs.php new file mode 100644 index 0000000000..626ea3786c --- /dev/null +++ b/actions/apistatusesfavs.php @@ -0,0 +1,134 @@ +. + * + * @category API + * @package GNUSocial + * @author Hannes Mannerheim + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://www.gnu.org/software/social/ + */ + +if (!defined('GNUSOCIAL')) { exit(1); } + +/** + * Show up to 100 favs of a notice + * + */ +class ApiStatusesFavsAction extends ApiAuthAction +{ + const MAXCOUNT = 100; + + var $original = null; + var $cnt = self::MAXCOUNT; + + /** + * Take arguments for running + * + * @param array $args $_REQUEST args + * + * @return boolean success flag + */ + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + $this->original = Notice::staticGet('id', $id); + + if (empty($this->original)) { + // TRANS: Client error displayed trying to display redents of a non-exiting notice. + $this->clientError(_('No such notice.'), + 400, $this->format); + return false; + } + + $cnt = $this->trimmed('count'); + + if (empty($cnt) || !is_integer($cnt)) { + $cnt = 100; + } else { + $this->cnt = min((int)$cnt, self::MAXCOUNT); + } + + return true; + } + + /** + * Handle the request + * + * Get favs and return them as json object + * + * @param array $args $_REQUEST data (unused) + * + * @return void + */ + function handle($args) + { + parent::handle($args); + + $fave = new Fave(); + $fave->selectAdd(); + $fave->selectAdd('user_id'); + $fave->notice_id = $this->original->id; + $fave->orderBy('modified'); + if (!is_null($this->cnt)) { + $fave->limit(0, $this->cnt); + } + + $ids = $fave->fetchAll('user_id'); + + // get nickname and profile image + $ids_with_profile_data = array(); + $i=0; + foreach($ids as $id) { + $profile = Profile::staticGet('id', $id); + $ids_with_profile_data[$i]['user_id'] = $id; + $ids_with_profile_data[$i]['nickname'] = $profile->nickname; + $ids_with_profile_data[$i]['fullname'] = $profile->fullname; + $ids_with_profile_data[$i]['profileurl'] = $profile->profileurl; + $profile = new Profile(); + $profile->id = $id; + $avatarurl = $profile->avatarUrl(24); + $ids_with_profile_data[$i]['avatarurl'] = $avatarurl; + $i++; + } + + $this->initDocument('json'); + $this->showJsonObjects($ids_with_profile_data); + $this->endDocument('json'); + } + + /** + * Return true if read only. + * + * MAY override + * + * @param array $args other arguments + * + * @return boolean is read only action? + */ + + function isReadOnly($args) + { + return true; + } +} diff --git a/actions/apistatusesshow.php b/actions/apistatusesshow.php index 9ab3c46e4c..67d04a505c 100644 --- a/actions/apistatusesshow.php +++ b/actions/apistatusesshow.php @@ -38,8 +38,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns the notice specified by id as a Twitter-style status and inline user * diff --git a/actions/apistatusnetversion.php b/actions/apistatusnetversion.php index 3a7b150cab..b40f42aa16 100644 --- a/actions/apistatusnetversion.php +++ b/actions/apistatusnetversion.php @@ -32,8 +32,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns a version number for this version of StatusNet, which * should make things a bit easier for upgrades. diff --git a/actions/apitimelinegroup.php b/actions/apitimelinegroup.php index 5a0ea60c7e..c238f3a989 100644 --- a/actions/apitimelinegroup.php +++ b/actions/apitimelinegroup.php @@ -35,8 +35,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns the most recent notices (default 20) posted to the group specified by ID * diff --git a/actions/apitimelinelist.php b/actions/apitimelinelist.php index c2339f9c35..6a3f6bfcc8 100644 --- a/actions/apitimelinelist.php +++ b/actions/apitimelinelist.php @@ -35,7 +35,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; require_once INSTALLDIR . '/lib/atomlistnoticefeed.php'; /** diff --git a/actions/apitimelinepublic.php b/actions/apitimelinepublic.php index b82e01aafe..338cd16fa0 100644 --- a/actions/apitimelinepublic.php +++ b/actions/apitimelinepublic.php @@ -38,8 +38,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns the most recent notices (default 20) posted by everybody * diff --git a/actions/apitimelinetag.php b/actions/apitimelinetag.php index 5bc330a26e..b3f17d0712 100644 --- a/actions/apitimelinetag.php +++ b/actions/apitimelinetag.php @@ -35,8 +35,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Returns the 20 most recent notices tagged by a given tag * @@ -179,7 +177,9 @@ class ApiTimelineTagAction extends ApiPrivateAuthAction $notice = Notice_tag::getStream( $this->tag, ($this->page - 1) * $this->count, - $this->count + 1 + $this->count + 1, + $this->since_id, + $this->max_id ); while ($notice->fetch()) { diff --git a/actions/apitimelineuser.php b/actions/apitimelineuser.php index 2540c036c1..10771fad73 100644 --- a/actions/apitimelineuser.php +++ b/actions/apitimelineuser.php @@ -229,7 +229,8 @@ class ApiTimelineUserAction extends ApiBareAuthAction $notice = $this->user->getNotices(($this->page-1) * $this->count, $this->count + 1, $this->since_id, - $this->max_id); + $this->max_id, + $this->scoped); while ($notice->fetch()) { if (count($notices) < $this->count) { diff --git a/actions/apitrends.php b/actions/apitrends.php index 3e854b1096..a39769a34e 100644 --- a/actions/apitrends.php +++ b/actions/apitrends.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/apiprivateauth.php'; - /** * Returns the top ten queries that are currently trending * diff --git a/actions/apiuserprofileimage.php b/actions/apiuserprofileimage.php index 81b447f7e7..a996fe1718 100644 --- a/actions/apiuserprofileimage.php +++ b/actions/apiuserprofileimage.php @@ -31,8 +31,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Ouputs avatar URL for a user, specified by screen name. * Unlike most API endpoints, this returns an HTTP redirect rather than direct data. diff --git a/actions/apiusershow.php b/actions/apiusershow.php index 0ea0b2e72f..e68047eb29 100644 --- a/actions/apiusershow.php +++ b/actions/apiusershow.php @@ -34,8 +34,6 @@ if (!defined('STATUSNET')) { exit(1); } -require_once INSTALLDIR . '/lib/apiprivateauth.php'; - /** * Ouputs information for a user, specified by ID or screen name. * The user's most recent status will be returned inline. diff --git a/classes/Notice.php b/classes/Notice.php index 13b3799385..4812dff91a 100644 --- a/classes/Notice.php +++ b/classes/Notice.php @@ -211,6 +211,11 @@ class Notice extends Managed_DataObject return $result; } + public function getUri() + { + return $this->uri; + } + /** * Extract #hashtags from this notice's content and save them to the database. */ @@ -417,7 +422,7 @@ class Notice extends Managed_DataObject $repeat = Notice::getKV('id', $repeat_of); - if (empty($repeat)) { + if (!($repeat instanceof Notice)) { // TRANS: Client exception thrown in notice when trying to repeat a missing or deleted notice. throw new ClientException(_('Cannot repeat; original notice is missing or deleted.')); } @@ -439,7 +444,7 @@ class Notice extends Managed_DataObject throw new ClientException(_('Cannot repeat a notice you cannot read.'), 403); } - if ($profile->hasRepeated($repeat->id)) { + if ($profile->hasRepeated($repeat)) { // TRANS: Client error displayed when trying to repeat an already repeated notice. throw new ClientException(_('You already repeated that notice.')); } @@ -1710,9 +1715,9 @@ class Notice extends Managed_DataObject // favorite and repeated if (!empty($cur)) { - $noticeInfoAttr['favorite'] = ($cur->hasFave($this)) ? "true" : "false"; $cp = $cur->getProfile(); - $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this->id)) ? "true" : "false"; + $noticeInfoAttr['favorite'] = ($cp->hasFave($this)) ? "true" : "false"; + $noticeInfoAttr['repeated'] = ($cp->hasRepeated($this)) ? "true" : "false"; } if (!empty($this->repeat_of)) { diff --git a/classes/Profile.php b/classes/Profile.php index c0af2635a7..e1bba076e9 100644 --- a/classes/Profile.php +++ b/classes/Profile.php @@ -211,9 +211,9 @@ class Profile extends Managed_DataObject return $stream->getNotices($offset, $limit, $since_id, $max_id); } - function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0) + function getNotices($offset=0, $limit=NOTICES_PER_PAGE, $since_id=0, $max_id=0, Profile $scoped=null) { - $stream = new ProfileNoticeStream($this); + $stream = new ProfileNoticeStream($this, $scoped); return $stream->getNotices($offset, $limit, $since_id, $max_id); } @@ -1157,12 +1157,13 @@ class Profile extends Managed_DataObject return $result; } - function hasRepeated($notice_id) + // FIXME: Can't put Notice typing here due to ArrayWrapper + public function hasRepeated($notice) { // XXX: not really a pkey, but should work $notice = Notice::pkeyGet(array('profile_id' => $this->id, - 'repeat_of' => $notice_id)); + 'repeat_of' => $notice->id)); return !empty($notice); } diff --git a/classes/User_group.php b/classes/User_group.php index 50f4b7ddc7..47ca3538e5 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -210,7 +210,12 @@ class User_group extends Managed_DataObject return $members; } - function getMemberCount() + public function getAdminCount() + { + return $this->getAdmins()->N; + } + + public function getMemberCount() { $key = sprintf("group:member_count:%d", $this->id); diff --git a/lib/action.php b/lib/action.php index 36f22a3da5..d16edfd804 100644 --- a/lib/action.php +++ b/lib/action.php @@ -62,7 +62,7 @@ class Action extends HTMLOutputter // lawsuit protected $menus = true; protected $needLogin = false; - // The currently scoped profile + // The currently scoped profile (normally Profile::current; from $this->auth_user for API) protected $scoped = null; // Messages to the front-end user diff --git a/lib/apiaction.php b/lib/apiaction.php index 781d106cf6..0bafd8fdbb 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -214,7 +214,23 @@ class ApiAction extends Action $twitter_user['location'] = ($profile->location) ? $profile->location : null; $twitter_user['description'] = ($profile->bio) ? $profile->bio : null; - $twitter_user['profile_image_url'] = $profile->avatarUrl(AVATAR_STREAM_SIZE); + // TODO: avatar url template (example.com/user/avatar?size={x}x{y}) + $twitter_user['profile_image_url'] = Avatar::urlByProfile($profile, AVATAR_STREAM_SIZE); + // START introduced by qvitter API, not necessary for StatusNet API + $twitter_user['profile_image_url_profile_size'] = Avatar::urlByProfile($profile, AVATAR_PROFILE_SIZE); + try { + $avatar = Avatar::getUploaded($profile); + $origurl = $avatar->displayUrl(); + } catch (Exception $e) { + $origurl = $twitter_user['profile_image_url_profile_size']; + } + $twitter_user['profile_image_url_original'] = $origurl; + + $twitter_user['groups_count'] = $profile->getGroups(0, null)->N; + foreach (array('linkcolor', 'backgroundcolor') as $key) { + $twitter_user[$key] = Profile_prefs::getConfigData($profile, 'theme', $key); + } + // END introduced by qvitter API, not necessary for StatusNet API $twitter_user['url'] = ($profile->homepage) ? $profile->homepage : null; $twitter_user['protected'] = (!empty($user) && $user->private_stream) ? true : false; @@ -263,7 +279,7 @@ class ApiAction extends Action if ($get_notice) { $notice = $profile->getCurrentNotice(); - if ($notice) { + if ($notice instanceof Notice) { // don't get user! $twitter_user['status'] = $this->twitterStatusArray($notice, false); } @@ -299,8 +315,12 @@ class ApiAction extends Action $twitter_status['text'] = $notice->content; $twitter_status['truncated'] = false; # Not possible on StatusNet $twitter_status['created_at'] = $this->dateTwitter($notice->created); - $twitter_status['in_reply_to_status_id'] = ($notice->reply_to) ? - intval($notice->reply_to) : null; + try { + $in_reply_to = $notice->getParent()->id; + } catch (Exception $e) { + $in_reply_to = null; + } + $twitter_status['in_reply_to_status_id'] = $in_reply_to; $source = null; @@ -317,6 +337,7 @@ class ApiAction extends Action } } + $twitter_status['uri'] = $notice->getUri(); $twitter_status['source'] = $source; $twitter_status['id'] = intval($notice->id); @@ -343,10 +364,12 @@ class ApiAction extends Action $twitter_status['geo'] = null; } - if (isset($this->auth_user)) { - $twitter_status['favorited'] = $this->auth_user->hasFave($notice); + if (!is_null($this->scoped)) { + $twitter_status['favorited'] = $this->scoped->hasFave($notice); + $twitter_status['repeated'] = $this->scoped->hasRepeated($notice); } else { $twitter_status['favorited'] = false; + $twitter_status['repeated'] = false; } // Enclosures @@ -399,6 +422,7 @@ class ApiAction extends Action ); } + $twitter_group['admin_count'] = $group->getAdminCount(); $twitter_group['member_count'] = $group->getMemberCount(); $twitter_group['original_logo'] = $group->original_logo; $twitter_group['homepage_logo'] = $group->homepage_logo; @@ -1550,6 +1574,8 @@ class ApiAction extends Action } else if (self::is_decimal($id)) { return User_group::getKV('id', $id); + } else if ($this->arg('uri')) { // FIXME: move this into empty($id) check? + return User_group::getKV('uri', urldecode($this->arg('uri'))); } else { return User_group::getForNickname($id); } diff --git a/lib/apiprivateauth.php b/lib/apiprivateauthaction.php similarity index 100% rename from lib/apiprivateauth.php rename to lib/apiprivateauthaction.php diff --git a/lib/jsonsearchresultslist.php b/lib/jsonsearchresultslist.php index 48540a4d58..357ab9be5d 100644 --- a/lib/jsonsearchresultslist.php +++ b/lib/jsonsearchresultslist.php @@ -232,14 +232,7 @@ class ResultItem $this->id = $this->notice->id; $this->from_user_id = $this->profile->id; - $user = $this->profile->getUser(); - - if (empty($user)) { - // Gonna have to do till we can detect it - $this->iso_language_code = common_config('site', 'language'); - } else { - $this->iso_language_code = $user->language; - } + $this->iso_language_code = Profile_prefs::getConfigData($this->profile, 'site', 'language'); $this->source = $this->getSourceLink($this->notice->source); diff --git a/lib/noticelistitem.php b/lib/noticelistitem.php index 8fa5c5dcd4..19d6674ae3 100644 --- a/lib/noticelistitem.php +++ b/lib/noticelistitem.php @@ -693,7 +693,7 @@ class NoticeListItem extends Widget $user->id != $this->notice->profile_id) { $this->out->text(' '); $profile = $user->getProfile(); - if ($profile->hasRepeated($this->notice->id)) { + if ($profile->hasRepeated($this->notice)) { $this->out->element('span', array('class' => 'repeated', // TRANS: Title for repeat form status in notice list when a notice has been repeated. 'title' => _('Notice repeated.')), diff --git a/lib/router.php b/lib/router.php index a7b59e6ab4..0d38d44835 100644 --- a/lib/router.php +++ b/lib/router.php @@ -460,6 +460,41 @@ class Router 'id' => '[0-9]+', 'format' => '(xml|json)')); + // START qvitter API additions + + $m->connect('api/statuses/favs/:id.json', + array('action' => 'ApiStatusesFavs', + 'id' => '[0-9]+')); + + $m->connect('api/attachment/:id.json', + array('action' => 'ApiAttachment', + 'id' => '[0-9]+')); + + $m->connect('api/checkhub.json', + array('action' => 'ApiCheckHub')); + + $m->connect('api/externalprofile/show.json', + array('action' => 'ApiExternalProfileShow')); + + $m->connect('api/statusnet/groups/admins/:id.:format', + array('action' => 'ApiGroupAdmins', + 'id' => Nickname::INPUT_FMT, + 'format' => '(xml|json)')); + + $m->connect('api/account/update_link_color.json', + array('action' => 'ApiAccountUpdateLinkColor')); + + $m->connect('api/account/update_background_color.json', + array('action' => 'ApiAccountUpdateBackgroundColor')); + + $m->connect('api/account/register.json', + array('action' => 'ApiAccountRegister')); + + $m->connect('api/check_nickname.json', + array('action' => 'ApiCheckNickname')); + + // END qvitter API additions + // users $m->connect('api/users/show/:id.:format', @@ -773,6 +808,7 @@ class Router // Tags $m->connect('api/statusnet/tags/timeline/:tag.:format', array('action' => 'ApiTimelineTag', + 'tag' => self::REGEX_TAG, 'format' => '(xml|json|rss|atom|as)')); // media related diff --git a/plugins/SubMirror/classes/SubMirror.php b/plugins/SubMirror/classes/SubMirror.php index 7e8d288b61..0920856192 100644 --- a/plugins/SubMirror/classes/SubMirror.php +++ b/plugins/SubMirror/classes/SubMirror.php @@ -148,10 +148,10 @@ class SubMirror extends Managed_DataObject * @param Notice $notice * @return mixed Notice on successful mirroring, boolean if not */ - public function mirrorNotice($notice) + public function mirrorNotice(Notice $notice) { $profile = Profile::getKV('id', $this->subscriber); - if (!$profile) { + if (!($profile instanceof Profile)) { common_log(LOG_ERR, "SubMirror plugin skipping auto-repeat of notice $notice->id for missing user $profile->id"); return false; } @@ -172,9 +172,9 @@ class SubMirror extends Managed_DataObject * @param Notice $notice * @return mixed Notice on successful repeat, true if already repeated, false on failure */ - protected function repeatNotice($profile, $notice) + protected function repeatNotice(Profile $profile, Notice $notice) { - if($profile->hasRepeated($notice->id)) { + if($profile->hasRepeated($notice)) { common_log(LOG_INFO, "SubMirror plugin skipping auto-repeat of notice $notice->id for user $profile->id; already repeated."); return true; } else {