From b1f337fe018dd3910c68589a98b0242470743a4c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Fri, 6 Mar 2009 13:33:47 -0800 Subject: [PATCH] First crack at Twitter-like JSON search results for the API --- ...witapisearch.php => twitapisearchjson.php} | 88 ++++-- lib/jsonsearchresultslist.php | 259 ++++++++++++++++++ lib/router.php | 10 +- lib/twitterapi.php | 28 +- 4 files changed, 357 insertions(+), 28 deletions(-) rename actions/{twitapisearch.php => twitapisearchjson.php} (54%) create mode 100644 lib/jsonsearchresultslist.php diff --git a/actions/twitapisearch.php b/actions/twitapisearchjson.php similarity index 54% rename from actions/twitapisearch.php rename to actions/twitapisearchjson.php index 822ee77e17..b50aa86b7a 100644 --- a/actions/twitapisearch.php +++ b/actions/twitapisearchjson.php @@ -27,11 +27,12 @@ * @link http://laconi.ca/ */ -if (!defined('LACONICA')) { +if (!defined('LACONICA')) { exit(1); } require_once INSTALLDIR.'/lib/twitterapi.php'; +require_once INSTALLDIR.'/lib/jsonsearchresultslist.php'; /** * Action handler for Twitter-compatible API search @@ -44,29 +45,52 @@ require_once INSTALLDIR.'/lib/twitterapi.php'; * @see TwitterapiAction */ -class TwitapisearchAction extends TwitterapiAction +class TwitapisearchjsonAction extends TwitterapiAction { - var $query; + var $lang; + var $rpp; + var $page; + var $since_id; var $limit; - var $callback; - + var $geocode; + /** * Initialization. * * @param array $args Web and URL arguments * - * @return boolean false if user doesn't exist + * @return boolean true if nothing goes wrong */ - + function prepare($args) { parent::prepare($args); - $qeury = $this->trimmed('query'); + + $this->query = $this->trimmed('q'); + $this->lang = $this->trimmed('lang'); + $this->rpp = $this->trimmed('rpp'); + + if (!$this->rpp) { + $this->rpp = 15; + } + + if ($this->rpp > 100) { + $this->rpp = 100; + } + + $this->page = $this->trimmed('page'); + + if (!$this->page) { + $this->page = 1; + } + + $this->since_id = $this->trimmed('since_id'); + $this->geocode = $this->trimmed('geocode'); return true; } - + /** * Handle a request * @@ -78,20 +102,48 @@ class TwitapisearchAction extends TwitterapiAction function handle($args) { parent::handle($args); - $this->showResults($this->limit); + $this->showResults(); } - + /** * Show search results * - * @param int $limit Number of notices to show - * * @return void */ - - function showResults($limit) + + function showResults() { - $this->serverError(_('API method under construction.'), $code = 501); + + // TODO: Support search operators like from: and to: + + $notice = new Notice(); + + // lcase it for comparison + $q = strtolower($this->query); + + $search_engine = $notice->getSearchEngine('identica_notices'); + $search_engine->set_sort_mode('chron'); + $search_engine->limit(($this->page - 1) * $this->rpp, $this->rpp + 1, true); + $search_engine->query($q); + $cnt = $notice->find(); + + // TODO: since_id, lang, geocode + + $results = new JSONSearchResultsList($notice, $q, $this->rpp, $this->page); + + $this->init_document('json'); + $results->show(); + $this->end_document('json'); } - -} + + /** + * This is a read-only action + * + * @return boolean true + */ + + function isReadOnly() + { + return true; + } +} \ No newline at end of file diff --git a/lib/jsonsearchresultslist.php b/lib/jsonsearchresultslist.php new file mode 100644 index 0000000000..171e1db4d6 --- /dev/null +++ b/lib/jsonsearchresultslist.php @@ -0,0 +1,259 @@ +. + * + * @category Search + * @package Laconica + * @author Zach Copley + * @copyright 2008-2009 Control Yourself, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + */ + +if (!defined('LACONICA')) { + exit(1); +} + +/** + * widget-like class for showing JSON search results + * + * @category Search + * @package Laconica + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * + */ + +class JSONSearchResultsList +{ + protected $notice; // protected attrs invisible to json_encode() + protected $rpp; + + // The below attributes are carefully named so the JSON output from + // this obj matches the output from search.twitter.com + + var $results; + var $since_id; + var $max_id; + var $refresh_url; + var $results_per_page; + var $completed_in; + var $page; + var $query; + + /** + * constructor + * + * @param Notice $notice stream of notices from DB_DataObject + */ + + function __construct($notice, $query, $rpp, $page, $since_id = 0) + { + $this->notice = $notice; + $this->query = urlencode($query); + $this->results_per_page = $this->rpp = $rpp; + $this->page = $page; + $this->since_id = $since_id; + $this->results = array(); + } + + /** + * show the list of search results + * + * @return int count of the search results listed. + */ + + function show() + { + $cnt = 0; + + $time_start = microtime(true); + + while ($this->notice->fetch() && $cnt <= $this->rpp) { + $cnt++; + + // XXX: Hmmm. this depends on desc sort order + if (!$this->max_id) { + $this->max_id = (int)$this->notice->id; + } + + if ($cnt > $this->rpp) { + break; + } + + $item = new ResultItem($this->notice); + array_push($this->results, $item); + } + + $time_end = microtime(true); + $this->completed_in = $time_end - $time_start; + + // Set other attrs + + $this->refresh_url = '?since_id=' . $this->max_id . + '&q=' . $this->query; + + // pagination stuff + + if ($cnt > $this->rpp) { + $this->next_page = '?page=' . ($this->page + 1) . + '&max_id=' . $this->max_id; + if ($this->rpp != 15) { + $this->next_page .= '&rpp=' . $this->rpp; + } + $this->next_page .= '&q=' . $this->query; + } + + if ($this->page > 1) { + $this->previous_page = '?page=' . ($this->page - 1) . + '&max_id=' . $this->max_id; + if ($this->rpp != 15) { + $this->previous_page .= '&rpp=' . $this->rpp; + } + $this->previous_page .= '&q=' . $this->query; + } + + print json_encode($this); + + return $cnt; + } +} + +/** + * widget for displaying a single JSON search result + * + * @category UI + * @package Laconica + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://laconi.ca/ + * @see JSONSearchResultsList + */ + +class ResultItem +{ + /** The notice this item is based on. */ + + protected $notice; // protected attrs invisible to json_encode() + + /** The profile associated with the notice. */ + + protected $profile; + + // The below attributes are carefully named so the JSON output from + // this obj matches the output from search.twitter.com + + var $text; + var $to_user_id; + var $to_user; + var $from_user; + var $id; + var $from_user_id; + var $iso_language_code; + var $source; + var $profile_image_url; + var $created_at; + + /** + * constructor + * + * Also initializes the profile attribute. + * + * @param Notice $notice The notice we'll display + */ + + function __construct($notice) + { + $this->notice = $notice; + $this->profile = $notice->getProfile(); + $this->buildResult(); + } + + /** + * Build a search result object + * + * This populates the the result in preparation for JSON encoding. + * + * @return void + */ + + function buildResult() + { + $this->text = $this->notice->content; + $replier_profile = null; + + if ($this->notice->reply_to) { + $reply = Notice::staticGet(intval($notice->reply_to)); + if ($reply) { + $replier_profile = $reply->getProfile(); + } + } + + $this->to_user_id = ($replier_profile) ? + intval($replier_profile->id) : null; + $this->to_user = ($replier_profile) ? + $replier_profile->nickname : null; + $this->from_user = $this->profile->nickname; + $this->id = $this->notice->id; + $this->from_user_id = $this->profile->id; + + $user = User::staticGet('id', $this->profile->id); + $this->iso_language_code = $this->user->language; + + $this->source = $this->getSourceLink($this->notice->source); + + $avatar = $this->profile->getAvatar(AVATAR_STREAM_SIZE); + $this->profile_image_url = ($avatar) ? + $avatar->displayUrl() : Avatar::defaultImage(AVATAR_STREAM_SIZE); + + $this->created_at = date('r', $this->notice->created); + } + + /** + * Show the source of the notice + * + * Either the name (and link) of the API client that posted the notice, + * or one of other other channels. + * + * @return string the source of the Notice + */ + + function getSourceLink($source) + { + $source_name = _($source); + switch ($source) { + case 'web': + case 'xmpp': + case 'mail': + case 'omb': + case 'api': + break; + default: + $ns = Notice_source::staticGet($source); + if ($ns) { + $source_name = '' . $ns->name . ''; + } + break; + } + return $source_name; + } + +} diff --git a/lib/router.php b/lib/router.php index 41c376a725..516b481227 100644 --- a/lib/router.php +++ b/lib/router.php @@ -354,17 +354,13 @@ class Router // search - - foreach (array('json', 'atom') as $e) { - $m->connect('api/search.'.$e, - array('action' => 'twitapisearch')); - } - + $m->connect('api/search.atom', array('action' => 'twitapisearchatom')); + $m->connect('api/search.json', array('action' => 'twitapisearchjson')); $m->connect('api/trends.json', array('action' => 'twitapitrends')); // user stuff - foreach (array('subscriptions', 'subscribers', + foreach (array('subscriptions', 'subscribers', 'nudge', 'xrds', 'all', 'foaf', 'replies', 'inbox', 'outbox', 'microsummary') as $a) { $m->connect(':nickname/'.$a, diff --git a/lib/twitterapi.php b/lib/twitterapi.php index 74f265cbb9..1de169a0b1 100644 --- a/lib/twitterapi.php +++ b/lib/twitterapi.php @@ -24,11 +24,33 @@ class TwitterapiAction extends Action var $auth_user; + /** + * Initialization. + * + * @param array $args Web and URL arguments + * + * @return boolean false if user doesn't exist + */ + + function prepare($args) + { + parent::prepare($args); + return true; + } + + /** + * Handle a request + * + * @param array $args Arguments from $_REQUEST + * + * @return void + */ + function handle($args) { parent::handle($args); } - + function twitter_user_array($profile, $get_notice=false) { @@ -86,7 +108,7 @@ class TwitterapiAction extends Action ($replier_profile) ? $replier_profile->nickname : null; if (isset($this->auth_user)) { - $twitter_status['favorited'] = + $twitter_status['favorited'] = ($this->auth_user->hasFave($notice)) ? 'true' : 'false'; } else { $twitter_status['favorited'] = 'false'; @@ -399,7 +421,7 @@ class TwitterapiAction extends Action $t = strtotime($dt); return date("D M d G:i:s O Y", $t); } - + // XXX: Candidate for a general utility method somewhere? function count_subscriptions($profile) {