<?php

/**
 * A simple interface to Twitter Search API
 * Author: fbparis@gmail.com (http://fbparis.com/)
 * Please take a look on http://dev.twitter.com/doc/get/search before using this class
 *
 * UPDATE:
 *  Search results are no more filtered by default with check_lang() method if $checkLangMethod is set
 *  You have to manually call check_lang($text) method on a tweet to verify the tweet is matching to $lang
 *
 * EXAMPLE:
 *  A quick example with Google check lang method
 *
    $S = new TwitterSearch('fr');
    $S->checkLangMethod = 'google';
    
    $S->query('anal');
    
    if ($r = $S->fetch()) {
        foreach ($r as $s) {
            printf("%s ==> %s\n",$s->text,$S->check_lang($s->text) ? 'OK' : 'Rejected');
        }
    } 
 *
 */

class TwitterSearch {
    public 
$host 'http://search.twitter.com/search.json';
    public 
$safeMode true// a basic safe mode which can prevent your bot to explode twitter api rate limits
    
public $checkLangMethod ''// Allowed values are: blank (no verification), "google" or "alchemy"
    
    /**
     * May be usefull if $this->checkLangMethod is set to "google".
     * Apply for a key here: http://code.google.com/apis/ajaxsearch/signup.html
     */
    
public $google_api_key ''
    
    
/**
     * Required if $this->checkLangMethod is set to "alchemy".
     * Apply for a key here: http://www.alchemyapi.com/api/register.html
     */
    
public $alchemy_api_key '';
    
    public 
$lastUrl ''// last accessed url
    
public $lastResponseHeaders = array(); // last HTTP response headers
    
public $lastResponseCode 0// last HTTP response code
    
public $lastResponse ''// last response, the raw version
    
    
public $lang ''// Restricts tweets to the given language, given by an ISO 639-1 code
    
public $rpp 20// Results per page
    
public $show_user ''// When "true", prepends ":" to the beginning of the tweet
    
    /**
     * If you need to set the next 4 parameters, you have to update them AFTER calling the query() method
     */
    
public $since_id ''// Returns results with an ID greater than (that is, more recent than) the specified ID
    
public $max_id ''// Returns results with an ID lower than (that is, less recent than) the specified ID
    
public $until ''// Returns tweets generated before the given date. Date should be formatted as YYYY-MM-DD
    
public $page 1// The page number (starting at 1) to return, up to a max of roughly 1500 results (based on rpp * page)

    /**
     * Specifies what type of search results you would prefer to receive.
     * Valid values include:
     *  - mixed: Include both popular and real time results in the response.
     *  - recent: return only the most recent results in the response
     *  - popular: return only the most popular results in the response.
     */
    
public $result_type 'mixed';
    
    
/**
     * Returns tweets by users located within a given radius of the given latitude/longitude.
     * The location is preferentially taking from the Geotagging API, but will fall back to their Twitter profile.
     * The parameter value is specified by "latitude,longitude,radius", where radius units must be specified as either "mi" (miles) or "km" (kilometers).
     * Note that you cannot use the near operator via the API to geocode arbitrary locations;
     * however you can use this geocode parameter to search near geocodes directly.
     */
    
public $geocode '';
    
    public 
$source ''// Filter by twitter client (web, Echofon, etc.)
    
    
public $warning ''// Set if some warnings are returned by Twitter Search API
    
    
protected $cache = array(); // used for lang detection APIS
    
protected $cacheFile ''// If set, lang detection cache will be saved between sessions
    
protected $cacheSize 1000// Max number of elements in the lang detection cache array
        
    
protected $q ''// store the last query
    
protected $refresh_url '';
    protected 
$next_page '';
    
    
/**
     * Constructor
     * Lang parameter restricts tweets to the given language, given by an ISO 639-1 code
     * CacheFile: if set, cached data will be loaded and file will be set to store cache (if writable)
     */
    
function __construct($lang='',$cacheFile='') {
        
$this->context stream_context_create(stream_context_get_options(stream_context_get_default()));
        
$this->http_option('max_redirects',1);
        if (
$lang$this->lang $lang;
        if (
$cacheFile$this->cache_file($cacheFile);
    }
    
    
/**
     * Get or set the current cache file
     * On set, return "true" if cache file is writable and "false" if not
     */
    
public function cache_file($cacheFile=null) {
        if (
$cacheFile === null) return $this->cacheFile;
        if (
file_exists($cacheFile)) {
            list(
$this->cacheSize,$this->cache) = unserialize(file_get_contents($cacheFile));
        }
        
$this->cacheFile = @touch($cacheFile) ? $cacheFile '';
        return 
$this->cacheFile $this->save_cache() : false;
    }
    
    
/**
     * Get or set the current cache size (maximum number of elements for the $cache array)
     * On set, will try to save the cache
     */
    
public function cache_size($cacheSize=null) {
        if (
$cacheSize === null) return $this->cacheSize;
        if (
is_int($cacheSize) && ($cacheSize 0)) {
            
$this->cacheSize $cacheSize;
            return 
$this->save_cache();
        }
        return 
false;
    }
    
    
/**
     * Prevents cache to be greater than $cacheSize and save the cache to file if $cacheFile is defined
     */
    
function save_cache() {
        if (
count($this->cache) > $this->cacheSize$this->cache array_slice($this->cache,0,$this->cacheSize,true);
        if (
$this->cacheFile) {
            return @
file_put_contents($this->cacheFile,serialize(array($this->cacheSize,$this->cache)));
        }
        return 
true;
    }    
    
    
/**
     * You can use this method to get or set some http context options (see http://php.net/manual/en/context.http.php)
     * For example, to set the user agent to "Mozilla/5" call $object->http_option('user_agent','Mozilla/5')
     * If you must set non http options (ie: socket), you can use directly the stream_context_get_options() function with $object->context as context
     */
    
public function http_option($key,$value=null) {
        if (
$value === null) { // return the current value for this key
            
$options stream_context_get_options($this->context);
            return @
$options['http'][$key] ? $options['http'][$key] : '';
        } else { 
// set the option, return false if failed
            
return stream_context_set_option($this->context,'http',$key,$value);
        }
    }
            
    
/**
     * Set the query and reset $page, $since_id, $max_id, $until, $refresh_url and $next_page
     */
    
public function query($q) {
        
$this->trim($q);
        
$this->page 1;
        
$this->since_id '';
        
$this->max_id '';
        
$this->until '';
        
$this->next_page '';
        
$this->refresh_url '';
    }
    
    
/**
     * Check if $text is matching $lang
     * Will returns null if $checkLangMethod is invalid or $lang undefined
     * Note that on failure with lang detection APIS, will return 0
     * Returns null, 0 (false), 1 (true, from cache) or 2 (true, new)
     */
    
public function check_lang($text) {
        if (!
trim($text) || !$this->checkLangMethod || !$this->lang) return null;
        
$md5 '';
        if (
count($this->cache)) {
            
$md5 md5($text);
            if (
array_key_exists($md5,$this->cache)) {
                if (
$this->cache[$md5] == $this->lang) return 1;
            } 
        }
        
$lang 0;
        switch (
$this->checkLangMethod) {
        case 
'google':
            
$response json_decode($this->http('http://ajax.googleapis.com/ajax/services/language/detect?v=1.0&q=' urlencode($text) . ($this->google_api_key "&key=$this->google_api_key''),'GET'));
            if (
is_object($response) && property_exists($response,'responseData') && is_object($response->responseData) && property_exists($response->responseData,'language')) {
                
$lang = (string) $response->responseData->language;
                if (!
$md5$md5 md5($text);
            }
            break;
        case 
'alchemy':
            if (
$this->alchemy_api_key) {
                
$response json_decode($this->http('http://access.alchemyapi.com/calls/text/TextGetLanguage','POST',http_build_query(array(
                    
'apikey'=>$this->alchemy_api_key,
                    
'text'=>$text,
                    
'outputMode'=>'json'
                
))));
                if (
is_object($response) && property_exists($response,'iso-639-1')) {
                    
$lang = (string) $response->{'iso-639-1'};
                    if (!
$md5$md5 md5($text);
                }
            }
            break;
        default:
            
$this->checkLangMethod '';
            return 
null;
        }
        if (
$lang) {
            
$this->cache = array($md5=>$lang) + $this->cache;
            
$this->save_cache();
        }
        return (
$lang == $this->lang) ? 0;
    }

    
/**
     * Return results for the requested query and moves the internal data pointer ahead
     * Return "false" if no query defined, or if no more results found
     * Note that an empty array can be returned after language filtering
     */
    
public function fetch() {
        if (!
$this->q) return false;
        if (!
$this->refresh_url$this->make_query();
        if (!
$this->next_page) return false;
        
$response $this->http("{$this->host}{$this->next_page}",'GET');
        
$r = @json_decode(trim(preg_replace('#((id)|(cursor))":(\d+)#','\1":"\4"',$response)));
        if (
is_object($r)) {
            if (
property_exists($r,'refresh_url')) $this->refresh_url $r->refresh_url;
            
$this->next_page property_exists($r,'next_page') ? $r->next_page '';
            
$this->page property_exists($r,'page') ? $r->page '';
            
$this->max_id property_exists($r,'max_id') ? $r->max_id '';
            
$this->since_id property_exists($r,'since_id') ? $r->since_id '';
            
$this->warning property_exists($r,'warning') ? $r->warning '';
            
            return 
property_exists($r,'results') ? $r->results : array();
        }
        return 
false;
    }
    
    
/**
     * Refresh the current query and return the results
     */
    
public function refresh() {
        if (!
$this->max_id) {
            if (!
$this->refresh_url) return false;
            
$this->next_url $this->refresh_url;
        } else {
            
$this->since_id $this->max_id;
            
$this->max_id '';
            
$this->until '';
            
$this->next_page '';
            
$this->refresh_url '';
            
$this->make_query();
        }
        return 
$this->fetch();
    }
    
    
/**
     * Format the query
     */
    
protected function make_query() {
        
$p = array('q'=>$this->q);
        if (
$this->page$p['page'] = $page;
        if (
$this->since_id$p['since_id'] = $this->since_id;
        if (
$this->max_id$p['max_id'] = $this->max_id;
        if (
$this->until$p['until'] = $this->until;
        if (
$this->rpp$p['rpp'] = $this->rpp;
        if (
$this->geocode$p['geocode'] = $this->geocode;
        if (
$this->source$p['source'] = $this->source;
        if (
$this->lang$p['lang'] = $this->lang;
        if (
$this->result_type$p['result_type'] = $this->result_type;
        if (
$this->show_user$p['show_user'] = $this->show_user;
        
$this->refresh_url $this->next_page sprintf('?%s',http_build_query($p));
    }

    
/**
     * Make an HTTP request
     * Infinite loops of redirections are not handled...
     * Last URL opened will be notified in $object->lastUrl
     * Last HTTP Response Code will be notified in $object->lastResponseCode (can be 0 if the request has failed)
     * Last HTTP Response Headers will be notified in $object->lastResponseHeaders (see http://php.net/manual/en/reserved.variables.httpresponseheader.php)
     * You can access the last raw response via $object->lastResponse
     */ 
    
protected function http($url,$method,$postfields='') {
        
$this->lastResponseCode 0;
        
$this->http_option('method',$method);
        
$this->http_option('content',$postfields);
        
$response = @file_get_contents($url,false,$this->context);
        
$this->lastUrl $url;
        
$this->lastResponseHeaders $http_response_header;
        
$this->lastResponse $response;
        if (
is_array($this->lastResponseHeaders)) {
            foreach (
$this->lastResponseHeaders as $i=>$header) {
                if (
$i == 0$this->lastResponseCode preg_match('#^HTTP/[0-9.]+ ([0-9]+)#s',$header,$m) ? intval($m[1]) : 0;
                elseif (
preg_match('#^Location: (.*)$#s',$header,$m)) return $this->http(trim($m[1]),'GET');
            }
        } else 
$this->lastResponseHeaders = array();
        return 
$response;
    }
}

?>