├── LICENSE ├── README.md ├── config.php ├── index.php └── lib ├── DatumboxAPI.php ├── TwitterSentimentAnalysis.php ├── ca-chain-bundle.crt └── twitter-client.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Datumbox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Twitter-Sentiment-Analysis 2 | ========================== 3 | 4 | This tool is written in PHP and it performs Sentiment Analysis on Twitter messages by using the Datumbox API 1.0v. 5 | 6 | To read more about how it works, how it should be configured etc check out the original blog post: 7 | http://blog.datumbox.com/how-to-build-your-own-twitter-sentiment-analysis-tool/ 8 | 9 | Useful Links 10 | ============ 11 | 12 | Download API Documentation and Code Samples: http://www.datumbox.com/machine-learning-api/ 13 | 14 | Sign-up for free API Key: http://www.datumbox.com/users/register/ 15 | 16 | View your API Key: http://www.datumbox.com/apikeys/view/ 17 | 18 | PHP Twitter API Client: https://github.com/timwhitlock/php-twitter-api -------------------------------------------------------------------------------- /config.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Datumbox Twitter Sentiment Analysis Demo 8 | 9 | 10 |

Datumbox Twitter Sentiment Analysis

11 |

Type your keyword below to perform Sentiment Analysis on Twitter Results:

12 |
13 | 14 | 15 |
16 | 17 | $_GET['q'], 28 | 'lang'=>'en', 29 | 'count'=>10, 30 | ); 31 | $results=$TwitterSentimentAnalysis->sentimentAnalysis($twitterSearchParams); 32 | 33 | 34 | ?> 35 |

Results for ""

36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 68 |
IdUserTextTwitter LinkSentiment
View
69 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /lib/DatumboxAPI.php: -------------------------------------------------------------------------------- 1 | api_key=$api_key; 22 | } 23 | 24 | /** 25 | * Calls the Web Service of Datumbox 26 | * 27 | * @param string $api_method 28 | * @param array $POSTparameters 29 | * 30 | * @return string $jsonreply 31 | */ 32 | protected function CallWebService($api_method,$POSTparameters) { 33 | $POSTparameters['api_key']=$this->api_key; 34 | 35 | $ch = curl_init(); 36 | curl_setopt($ch, CURLOPT_URL, 'http://api.datumbox.com/'.self::version.'/'.$api_method.'.json'); 37 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 38 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); 39 | curl_setopt($ch, CURLOPT_ENCODING, 'gzip,deflate'); 40 | curl_setopt($ch, CURLOPT_POST, true ); 41 | curl_setopt($ch, CURLOPT_POSTFIELDS, $POSTparameters); 42 | 43 | $jsonreply = curl_exec ($ch); 44 | curl_close ($ch); 45 | unset($ch); 46 | 47 | return $jsonreply; 48 | } 49 | 50 | /** 51 | * Parses the API Reply 52 | * 53 | * @param mixed $jsonreply 54 | * 55 | * @return mixed 56 | */ 57 | protected function ParseReply($jsonreply) { 58 | $jsonreply=json_decode($jsonreply,true); 59 | 60 | if(isset($jsonreply['output']['status']) && $jsonreply['output']['status']==1) { 61 | return $jsonreply['output']['result']; 62 | } 63 | 64 | if(isset($jsonreply['error']['ErrorCode']) && isset($jsonreply['error']['ErrorMessage'])) { 65 | echo $jsonreply['error']['ErrorMessage'].' (ErrorCode: '.$jsonreply['error']['ErrorCode'].')'; 66 | } 67 | 68 | return false; 69 | } 70 | 71 | /** 72 | * Performs Sentiment Analysis. 73 | * 74 | * @param string $text The clear text (no HTML tags) that we evaluate. 75 | * 76 | * @return string|false It returns "positive", "negative" or "neutral" on success and false on fail. 77 | */ 78 | public function SentimentAnalysis($text) { 79 | $parameters=array( 80 | 'text'=>$text, 81 | ); 82 | 83 | $jsonreply=$this->CallWebService('SentimentAnalysis',$parameters); 84 | 85 | return $this->ParseReply($jsonreply); 86 | } 87 | 88 | /** 89 | * Performs Sentiment Analysis on Twitter. 90 | * 91 | * @param string $text The text of the tweet that we evaluate. 92 | * 93 | * @return string|false It returns "positive", "negative" or "neutral" on success and false on fail. 94 | */ 95 | public function TwitterSentimentAnalysis($text) { 96 | $parameters=array( 97 | 'text'=>$text, 98 | ); 99 | 100 | $jsonreply=$this->CallWebService('TwitterSentimentAnalysis',$parameters); 101 | 102 | return $this->ParseReply($jsonreply); 103 | } 104 | 105 | /** 106 | * Performs Subjectivity Analysis. 107 | * 108 | * @param string $text The clear text (no HTML tags) that we evaluate. 109 | * 110 | * @return string|false. It returns "objective" or "subjective" on success and false on fail. 111 | */ 112 | public function SubjectivityAnalysis($text) { 113 | $parameters=array( 114 | 'text'=>$text, 115 | ); 116 | 117 | $jsonreply=$this->CallWebService('SubjectivityAnalysis',$parameters); 118 | 119 | return $this->ParseReply($jsonreply); 120 | } 121 | 122 | /** 123 | * Performs Topic Classification. 124 | * 125 | * @param string $text The clear text (no HTML tags) that we evaluate. 126 | * 127 | * @return string|false. It returns "Arts", "Business & Economy", "Computers & Technology", "Health", "Home & Domestic Life", "News", "Recreation & Activities", "Reference & Education", "Science", "Shopping", "Society", "Sports" on success and false on fail. 128 | */ 129 | public function TopicClassification($text) { 130 | $parameters=array( 131 | 'text'=>$text, 132 | ); 133 | 134 | $jsonreply=$this->CallWebService('TopicClassification',$parameters); 135 | 136 | return $this->ParseReply($jsonreply); 137 | } 138 | 139 | /** 140 | * Performs Spam Detection. 141 | * 142 | * @param string $text The clear text (no HTML tags) that we evaluate. 143 | * 144 | * @return string|false It returns "spam" or "nospam" on success and false on fail. 145 | */ 146 | public function SpamDetection($text) { 147 | $parameters=array( 148 | 'text'=>$text, 149 | ); 150 | 151 | $jsonreply=$this->CallWebService('SpamDetection',$parameters); 152 | 153 | return $this->ParseReply($jsonreply); 154 | } 155 | 156 | /** 157 | * Performs Adult Content Detection. 158 | * 159 | * @param string $text The clear text (no HTML tags) that we evaluate. 160 | * 161 | * @return string|false It returns "adult" or "noadult" on success and false on fail. 162 | */ 163 | public function AdultContentDetection($text) { 164 | $parameters=array( 165 | 'text'=>$text, 166 | ); 167 | 168 | $jsonreply=$this->CallWebService('AdultContentDetection',$parameters); 169 | 170 | return $this->ParseReply($jsonreply); 171 | } 172 | 173 | /** 174 | * Performs Readability Assessment. 175 | * 176 | * @param string $text The clear text (no HTML tags) that we evaluate. 177 | * 178 | * @return string|false It returns "basic", "intermediate" or "advanced" on success and false on fail. 179 | */ 180 | public function ReadabilityAssessment($text) { 181 | $parameters=array( 182 | 'text'=>$text, 183 | ); 184 | 185 | $jsonreply=$this->CallWebService('ReadabilityAssessment',$parameters); 186 | 187 | return $this->ParseReply($jsonreply); 188 | } 189 | 190 | /** 191 | * Performs Language Detection. 192 | * 193 | * @param string $text The clear text (no HTML tags) that we evaluate. 194 | * 195 | * @return string|false It returns the ISO639-1 two-letter language code (http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) on success and false on fail. 196 | */ 197 | public function LanguageDetection($text) { 198 | $parameters=array( 199 | 'text'=>$text, 200 | ); 201 | 202 | $jsonreply=$this->CallWebService('LanguageDetection',$parameters); 203 | 204 | return $this->ParseReply($jsonreply); 205 | } 206 | 207 | /** 208 | * Performs Commercial Detection. 209 | * 210 | * @param string $text The clear text (no HTML tags) that we evaluate. 211 | * 212 | * @return string|false It returns "commercial" or "noncommercial" on success and false on fail. 213 | */ 214 | public function CommercialDetection($text) { 215 | $parameters=array( 216 | 'text'=>$text, 217 | ); 218 | 219 | $jsonreply=$this->CallWebService('CommercialDetection',$parameters); 220 | 221 | return $this->ParseReply($jsonreply); 222 | } 223 | 224 | /** 225 | * Performs Educational Detection. 226 | * 227 | * @param string $text The clear text (no HTML tags) that we evaluate. 228 | * 229 | * @return string|false It returns "educational" or "noneducational" on success and false on fail. 230 | */ 231 | public function EducationalDetection($text) { 232 | $parameters=array( 233 | 'text'=>$text, 234 | ); 235 | 236 | $jsonreply=$this->CallWebService('EducationalDetection',$parameters); 237 | 238 | return $this->ParseReply($jsonreply); 239 | } 240 | 241 | /** 242 | * Performs Gender Detection. 243 | * 244 | * @param string $text The clear text (no HTML tags) that we evaluate. 245 | * 246 | * @return string|false It returns "male" or "female" on success and false on fail. 247 | */ 248 | public function GenderDetection($text) { 249 | $parameters=array( 250 | 'text'=>$text, 251 | ); 252 | 253 | $jsonreply=$this->CallWebService('GenderDetection',$parameters); 254 | 255 | return $this->ParseReply($jsonreply); 256 | } 257 | 258 | /** 259 | * Performs Text Extraction. It extracts the important information (clear text) from a given webpage. 260 | * 261 | * @param string $text The HTML of the webpage. 262 | * 263 | * @return string|false It returns the clear text of the document on success and false on fail. 264 | */ 265 | public function TextExtraction($text) { 266 | $parameters=array( 267 | 'text'=>$text, 268 | ); 269 | 270 | $jsonreply=$this->CallWebService('TextExtraction',$parameters); 271 | 272 | return $this->ParseReply($jsonreply); 273 | } 274 | 275 | /** 276 | * Performs Keyword Extraction. It extracts the keywords and keywords combinations from a text. 277 | * 278 | * @param string $text The clear text (no HTML tags) that we analyze. 279 | * @param integer $n It is a number from 1 to 5 which denotes the number of Keyword combinations that we want to get. 280 | * 281 | * @return array|false It returns an array with the keywords of the document on success and false on fail. 282 | */ 283 | public function KeywordExtraction($text,$n) { 284 | $parameters=array( 285 | 'text'=>$text, 286 | 'n'=>$n, 287 | ); 288 | 289 | $jsonreply=$this->CallWebService('KeywordExtraction',$parameters); 290 | 291 | return $this->ParseReply($jsonreply); 292 | } 293 | 294 | /** 295 | * Evaluates the Document Similarity between 2 documents. 296 | * 297 | * @param string $original The first clear text (no HTML tags) that we compare. 298 | * @param string $copy The second clear text (no HTML tags) that we compare. 299 | * 300 | * @return array|false It returns an array with similarity metrics for the two documents on success and false on fail. 301 | */ 302 | public function DocumentSimilarity($original,$copy) { 303 | $parameters=array( 304 | 'original'=>$original, 305 | 'copy'=>$copy, 306 | ); 307 | 308 | $jsonreply=$this->CallWebService('DocumentSimilarity',$parameters); 309 | 310 | return $this->ParseReply($jsonreply); 311 | } 312 | 313 | 314 | } 315 | 316 | -------------------------------------------------------------------------------- /lib/TwitterSentimentAnalysis.php: -------------------------------------------------------------------------------- 1 | datumbox_api_key=$datumbox_api_key; 26 | 27 | $this->consumer_key=$consumer_key; 28 | $this->consumer_secret=$consumer_secret; 29 | $this->access_key=$access_key; 30 | $this->access_secret=$access_secret; 31 | } 32 | 33 | /** 34 | * This function fetches the twitter list and evaluates their sentiment 35 | * 36 | * @param array $twitterSearchParams The Twitter Search Parameters that are passed to Twitter API. Read more here https://dev.twitter.com/docs/api/1.1/get/search/tweets 37 | * 38 | * @return array 39 | */ 40 | public function sentimentAnalysis($twitterSearchParams) { 41 | $tweets=$this->getTweets($twitterSearchParams); 42 | 43 | return $this->findSentiment($tweets); 44 | } 45 | 46 | /** 47 | * Calls the Search/tweets method of the Twitter API for particular Twitter Search Parameters and returns the list of tweets that match the search criteria. 48 | * 49 | * @param mixed $twitterSearchParams The Twitter Search Parameters that are passed to Twitter API. Read more here https://dev.twitter.com/docs/api/1.1/get/search/tweets 50 | * 51 | * @return array $tweets 52 | */ 53 | protected function getTweets($twitterSearchParams) { 54 | $Client = new TwitterApiClient(); //Use the TwitterAPIClient 55 | $Client->set_oauth ($this->consumer_key, $this->consumer_secret, $this->access_key, $this->access_secret); 56 | 57 | $tweets = $Client->call('search/tweets', $twitterSearchParams, 'GET' ); //call the service and get the list of tweets 58 | 59 | unset($Client); 60 | 61 | return $tweets; 62 | } 63 | 64 | protected function findSentiment($tweets) { 65 | $DatumboxAPI = new DatumboxAPI($this->datumbox_api_key); //initialize the DatumboxAPI client 66 | 67 | $results=array(); 68 | foreach($tweets['statuses'] as $tweet) { //foreach of the tweets that we received 69 | if(isset($tweet['metadata']['iso_language_code']) && $tweet['metadata']['iso_language_code']=='en') { //perform sentiment analysis only for the English Tweets 70 | $sentiment=$DatumboxAPI->TwitterSentimentAnalysis($tweet['text']); //call Datumbox service to get the sentiment 71 | 72 | if($sentiment!=false) { //if the sentiment is not false, the API call was successful. 73 | $results[]=array( //add the tweet message in the results 74 | 'id'=>$tweet['id_str'], 75 | 'user'=>$tweet['user']['name'], 76 | 'text'=>$tweet['text'], 77 | 'url'=>'https://twitter.com/'.$tweet['user']['name'].'/status/'.$tweet['id_str'], 78 | 79 | 'sentiment'=>$sentiment, 80 | ); 81 | } 82 | } 83 | 84 | } 85 | 86 | unset($tweets); 87 | unset($DatumboxAPI); 88 | 89 | return $results; 90 | } 91 | } 92 | 93 | 94 | -------------------------------------------------------------------------------- /lib/twitter-client.php: -------------------------------------------------------------------------------- 1 | cache_ttl = (int) $ttl; 81 | $this->cache_ns = $namespace; 82 | return $this; 83 | } 84 | trigger_error( 'Cannot enable Twitter API cache without APC extension' ); 85 | return $this->disable_cache(); 86 | } 87 | 88 | /** 89 | * Disable caching for susequent API calls 90 | * @return TwitterApiClient 91 | */ 92 | public function disable_cache(){ 93 | $this->cache_ttl = null; 94 | $this->cache_ns = null; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Test whether the client has full authentication data. 100 | * Warning: does not validate credentials 101 | * @return bool 102 | */ 103 | public function has_auth(){ 104 | return $this->AccessToken instanceof TwitterOAuthToken && $this->AccessToken->secret; 105 | } 106 | 107 | /** 108 | * Unset all logged in credentials - useful in error situations 109 | * @return TwitterApiClient 110 | */ 111 | public function deauthorize(){ 112 | $this->AccessToken = null; 113 | return $this; 114 | } 115 | 116 | 117 | /** 118 | * Set currently logged in user's OAuth access token 119 | * @param string consumer api key 120 | * @param string consumer secret 121 | * @param string access token 122 | * @param string access token secret 123 | * @return TwitterApiClient 124 | */ 125 | public function set_oauth( $consumer_key, $consumer_secret, $access_key = '', $access_secret = '' ){ 126 | $this->deauthorize(); 127 | $this->Consumer = new TwitterOAuthToken( $consumer_key, $consumer_secret ); 128 | if( $access_key && $access_secret ){ 129 | $this->AccessToken = new TwitterOAuthToken( $access_key, $access_secret ); 130 | } 131 | return $this; 132 | } 133 | 134 | 135 | /** 136 | * Set consumer oauth token by object 137 | * @param TwitterOAuthToken 138 | * @return TwitterApiClient 139 | */ 140 | public function set_oauth_consumer( TwitterOAuthToken $token ){ 141 | $this->Consumer = $token; 142 | return $this; 143 | } 144 | 145 | 146 | /** 147 | * Set access oauth token by object 148 | * @param TwitterOAuthToken 149 | * @return TwitterApiClient 150 | */ 151 | public function set_oauth_access( TwitterOAuthToken $token ){ 152 | $this->AccessToken = $token; 153 | return $this; 154 | } 155 | 156 | 157 | 158 | /** 159 | * Contact Twitter for a request token, which will be exchanged for an access token later. 160 | * @param string callback URL or "oob" for desktop apps (out of bounds) 161 | * @return TwitterOAuthToken Request token 162 | */ 163 | public function get_oauth_request_token( $oauth_callback = 'oob' ){ 164 | $params = $this->oauth_exchange( TWITTER_OAUTH_REQUEST_TOKEN_URL, compact('oauth_callback') ); 165 | return new TwitterOAuthToken( $params['oauth_token'], $params['oauth_token_secret'] ); 166 | } 167 | 168 | 169 | 170 | /** 171 | * Exchange request token for an access token after authentication/authorization by user 172 | * @param string verifier passed back from Twitter or copied out of browser window 173 | * @return TwitterOAuthToken Access token 174 | */ 175 | public function get_oauth_access_token( $oauth_verifier ){ 176 | $params = $this->oauth_exchange( TWITTER_OAUTH_ACCESS_TOKEN_URL, compact('oauth_verifier') ); 177 | $token = new TwitterOAuthToken( $params['oauth_token'], $params['oauth_token_secret'] ); 178 | $token->user = array ( 179 | 'id' => $params['user_id'], 180 | 'screen_name' => $params['screen_name'], 181 | ); 182 | return $token; 183 | } 184 | 185 | 186 | 187 | /** 188 | * Basic sanitation of api request arguments 189 | * @param array original params passed by client code 190 | * @return array sanitized params that we'll serialize 191 | */ 192 | private function sanitize_args( array $_args ){ 193 | // transform some arguments and ensure strings 194 | // no further validation is performed 195 | $args = array(); 196 | foreach( $_args as $key => $val ){ 197 | if( is_string($val) ){ 198 | $args[$key] = $val; 199 | } 200 | else if( true === $val ){ 201 | $args[$key] = 'true'; 202 | } 203 | else if( false === $val || null === $val ){ 204 | $args[$key] = 'false'; 205 | } 206 | else if( ! is_scalar($val) ){ 207 | throw new TwitterApiException( 'Invalid Twitter parameter ('.gettype($val).') '.$key, -1 ); 208 | } 209 | else { 210 | $args[$key] = (string) $val; 211 | } 212 | } 213 | return $args; 214 | } 215 | 216 | 217 | 218 | /** 219 | * Call API method over HTTP and return serialized data 220 | * @param string API method, e.g. "users/show" 221 | * @param array method arguments 222 | * @param string http request method 223 | * @return array unserialized data returned from Twitter 224 | * @throws TwitterApiException 225 | */ 226 | public function call( $path, array $args = array(), $http_method = 'GET' ){ 227 | $args = $this->sanitize_args( $args ); 228 | // Fetch response from cache if possible / allowed / enabled 229 | if( $http_method === 'GET' && isset($this->cache_ttl) ){ 230 | $cachekey = $this->cache_ns.$path.'_'.md5( serialize($args) ); 231 | if( preg_match('/^(\d+)-/', $this->AccessToken->key, $reg ) ){ 232 | $cachekey .= '_'.$reg[1]; 233 | } 234 | $data = apc_fetch( $cachekey ); 235 | if( is_array($data) ){ 236 | return $data; 237 | } 238 | } 239 | $http = $this->rest_request( $path, $args, $http_method ); 240 | // Deserialize response 241 | $status = $http['status']; 242 | $data = json_decode( $http['body'], true ); 243 | // unserializable array assumed to be serious error 244 | if( ! is_array($data) ){ 245 | $err = array( 246 | 'message' => $http['error'], 247 | 'code' => -1 248 | ); 249 | TwitterApiException::chuck( $err, $status ); 250 | } 251 | // else could be well-formed error 252 | if( isset( $data['errors'] ) ) { 253 | while( $err = array_shift($data['errors']) ){ 254 | $err['message'] = $err['message']; 255 | if( $data['errors'] ){ 256 | $message = sprintf('Twitter error #%d', $err['code'] ).' "'.$err['message'].'"'; 257 | trigger_error( $message, E_USER_WARNING ); 258 | } 259 | else { 260 | TwitterApiException::chuck( $err, $status ); 261 | } 262 | } 263 | } 264 | if( isset($cachekey) ){ 265 | apc_store( $cachekey, $data, $this->cache_ttl ); 266 | } 267 | return $data; 268 | } 269 | 270 | 271 | 272 | /** 273 | * Call API method over HTTP and return raw response data without caching 274 | * @param string API method, e.g. "users/show" 275 | * @param array method arguments 276 | * @param string http request method 277 | * @return array structure from http_request 278 | * @throws TwitterApiException 279 | */ 280 | public function raw( $path, array $args = array(), $http_method = 'GET' ){ 281 | $args = $this->sanitize_args( $args ); 282 | return $this->rest_request( $path, $args, $http_method ); 283 | } 284 | 285 | 286 | 287 | /** 288 | * Perform an OAuth request - these differ somewhat from regular API calls 289 | * @internal 290 | */ 291 | private function oauth_exchange( $endpoint, array $args ){ 292 | // build a post request and authenticate via OAuth header 293 | $params = new TwitterOAuthParams( $args ); 294 | $params->set_consumer( $this->Consumer ); 295 | if( $this->AccessToken ){ 296 | $params->set_token( $this->AccessToken ); 297 | } 298 | $params->sign_hmac( 'POST', $endpoint ); 299 | $conf = array ( 300 | 'method' => 'POST', 301 | 'headers' => array( 'Authorization' => $params->oauth_header() ), 302 | ); 303 | $http = self::http_request( $endpoint, $conf ); 304 | $body = trim( $http['body'] ); 305 | $stat = $http['status']; 306 | if( 200 !== $stat ){ 307 | // Twitter might respond as XML, but with an HTML content type for some reason 308 | if( 0 === strpos($body, 'error; 311 | } 312 | throw new TwitterApiException( $body, -1, $stat ); 313 | } 314 | parse_str( $body, $params ); 315 | if( ! is_array($params) || ! isset($params['oauth_token']) || ! isset($params['oauth_token_secret']) ){ 316 | throw new TwitterApiException( 'Malformed response from Twitter', -1, $stat ); 317 | } 318 | return $params; 319 | } 320 | 321 | 322 | 323 | 324 | /** 325 | * Sign and execute REST API call 326 | * @return array 327 | */ 328 | private function rest_request( $path, array $args, $http_method ){ 329 | // all calls must be authenticated in API 1.1 330 | if( ! $this->has_auth() ){ 331 | throw new TwitterApiException( 'Twitter client not authenticated', 0, 401 ); 332 | } 333 | // prepare HTTP request config 334 | $conf = array ( 335 | 'method' => $http_method, 336 | ); 337 | // build signed URL and request parameters 338 | $endpoint = TWITTER_API_BASE.'/'.$path.'.json'; 339 | $params = new TwitterOAuthParams( $args ); 340 | $params->set_consumer( $this->Consumer ); 341 | $params->set_token( $this->AccessToken ); 342 | $params->sign_hmac( $http_method, $endpoint ); 343 | if( 'GET' === $http_method ){ 344 | $endpoint .= '?'.$params->serialize(); 345 | } 346 | else { 347 | $conf['body'] = $params->serialize(); 348 | } 349 | $http = self::http_request( $endpoint, $conf ); 350 | // remember current rate limits for this endpoint 351 | $this->last_call = $path; 352 | if( isset($http['headers']['x-rate-limit-limit']) ) { 353 | $this->last_rate[$path] = array ( 354 | 'limit' => (int) $http['headers']['x-rate-limit-limit'], 355 | 'remaining' => (int) $http['headers']['x-rate-limit-remaining'], 356 | 'reset' => (int) $http['headers']['x-rate-limit-reset'], 357 | ); 358 | } 359 | return $http; 360 | } 361 | 362 | 363 | 364 | /** 365 | * Abstract HTTP call, currently just uses cURL extension 366 | * @return array e.g. { body: '', error: '', status: 200, headers: {} } 367 | */ 368 | public static function http_request( $endpoint, array $conf ){ 369 | $conf += array( 370 | 'body' => '', 371 | 'method' => 'GET', 372 | 'headers' => array(), 373 | ); 374 | 375 | $ch = curl_init(); 376 | curl_setopt( $ch, CURLOPT_URL, $endpoint ); 377 | curl_setopt( $ch, CURLOPT_TIMEOUT, TWITTER_API_TIMEOUT ); 378 | curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, TWITTER_API_TIMEOUT ); 379 | curl_setopt( $ch, CURLOPT_USERAGENT, TWITTER_API_USERAGENT ); 380 | curl_setopt( $ch, CURLOPT_HEADER, true ); 381 | curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true ); 382 | 383 | switch ( $conf['method'] ) { 384 | case 'GET': 385 | break; 386 | case 'POST': 387 | curl_setopt( $ch, CURLOPT_POST, true ); 388 | curl_setopt( $ch, CURLOPT_POSTFIELDS, $conf['body'] ); 389 | break; 390 | default: 391 | throw new TwitterApiException('Unsupported method '.$conf['method'] ); 392 | } 393 | 394 | foreach( $conf['headers'] as $key => $val ){ 395 | $headers[] = $key.': '.$val; 396 | } 397 | if( isset($headers) ) { 398 | curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers ); 399 | } 400 | 401 | // execute and parse response 402 | $response = curl_exec( $ch ); 403 | if ( 60 === curl_errno($ch) ) { // CURLE_SSL_CACERT 404 | curl_setopt( $ch, CURLOPT_CAINFO, __DIR__.'/ca-chain-bundle.crt'); 405 | $response = curl_exec($ch); 406 | } 407 | $status = curl_getinfo( $ch, CURLINFO_HTTP_CODE ); 408 | $headers = array(); 409 | $body = ''; 410 | if( $response && $status ){ 411 | list( $header, $body ) = preg_split('/\r\n\r\n/', $response, 2 ); 412 | if( preg_match_all('/^(Content[\w\-]+|X-Rate[^:]+):\s*(.+)/mi', $header, $r, PREG_SET_ORDER ) ){ 413 | foreach( $r as $match ){ 414 | $headers[ strtolower($match[1]) ] = $match[2]; 415 | } 416 | } 417 | curl_close($ch); 418 | } 419 | else { 420 | $error = curl_error( $ch ) or 421 | $error = 'No response from Twitter'; 422 | is_resource($ch) and curl_close($ch); 423 | throw new TwitterApiException( $error ); 424 | } 425 | return array ( 426 | 'body' => $body, 427 | 'status' => $status, 428 | 'headers' => $headers, 429 | ); 430 | } 431 | 432 | 433 | 434 | /** 435 | * Get current rate limit, if known. does not look it up 436 | */ 437 | public function last_rate_limit_data( $func = '' ){ 438 | $func or $func = $this->last_call; 439 | return isset($this->last_rate[$func]) ? $this->last_rate[$func] : array( 'limit' => 0 ); 440 | } 441 | 442 | 443 | /** 444 | * Get rate limit allowance for last endpoint request 445 | */ 446 | public function last_rate_limit_allowance( $func = '' ){ 447 | $data = $this->last_rate_limit_data($func); 448 | return isset($data['limit']) ? $data['limit'] : null; 449 | } 450 | 451 | 452 | /** 453 | * Get number of requests remaining this period for last endpoint request 454 | */ 455 | public function last_rate_limit_remaining( $func = '' ){ 456 | $data = $this->last_rate_limit_data($func); 457 | return isset($data['remaining']) ? $data['remaining'] : null; 458 | } 459 | 460 | 461 | /** 462 | * Get rate limit reset time for last endpoint request 463 | */ 464 | public function last_rate_limit_reset( $func = '' ){ 465 | $data = $this->last_rate_limit_data($func); 466 | return isset($data['reset']) ? $data['reset'] : null; 467 | } 468 | 469 | } 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | /** 478 | * Simple token class that holds key and secret 479 | * @internal 480 | */ 481 | class TwitterOAuthToken { 482 | 483 | public $key; 484 | public $secret; 485 | public $verifier; 486 | public $user; 487 | 488 | public function __construct( $key, $secret = '' ){ 489 | if( ! $key ){ 490 | throw new Exception( 'Invalid OAuth token - Key required even if secret is empty' ); 491 | } 492 | $this->key = $key; 493 | $this->secret = $secret; 494 | $this->verifier = ''; 495 | } 496 | 497 | public function get_authorization_url(){ 498 | return TWITTER_OAUTH_AUTHORIZE_URL.'?oauth_token='.rawurlencode($this->key); 499 | } 500 | 501 | public function get_authentication_url(){ 502 | return TWITTER_OAUTH_AUTHENTICATE_URL.'?oauth_token='.rawurlencode($this->key); 503 | } 504 | 505 | } 506 | 507 | 508 | 509 | 510 | 511 | /** 512 | * Class for compiling, signing and serializing OAuth parameters 513 | * @internal 514 | */ 515 | class TwitterOAuthParams { 516 | 517 | private $args; 518 | private $consumer_secret; 519 | private $token_secret; 520 | 521 | private static function urlencode( $val ){ 522 | return str_replace( '%7E', '~', rawurlencode($val) ); 523 | } 524 | 525 | public function __construct( array $args = array() ){ 526 | $this->args = $args + array ( 527 | 'oauth_version' => '1.0', 528 | ); 529 | } 530 | 531 | public function set_consumer( TwitterOAuthToken $Consumer ){ 532 | $this->consumer_secret = $Consumer->secret; 533 | $this->args['oauth_consumer_key'] = $Consumer->key; 534 | } 535 | 536 | public function set_token( TwitterOAuthToken $Token ){ 537 | $this->token_secret = $Token->secret; 538 | $this->args['oauth_token'] = $Token->key; 539 | } 540 | 541 | private function normalize(){ 542 | $flags = SORT_STRING | SORT_ASC; 543 | ksort( $this->args, $flags ); 544 | foreach( $this->args as $k => $a ){ 545 | if( is_array($a) ){ 546 | sort( $this->args[$k], $flags ); 547 | } 548 | } 549 | return $this->args; 550 | } 551 | 552 | public function serialize(){ 553 | $str = http_build_query( $this->args ); 554 | // PHP_QUERY_RFC3986 requires PHP >= 5.4 555 | $str = str_replace( array('+','%7E'), array('%20','~'), $str ); 556 | return $str; 557 | } 558 | 559 | public function sign_hmac( $http_method, $http_rsc ){ 560 | $this->args['oauth_signature_method'] = 'HMAC-SHA1'; 561 | $this->args['oauth_timestamp'] = sprintf('%u', time() ); 562 | $this->args['oauth_nonce'] = sprintf('%f', microtime(true) ); 563 | unset( $this->args['oauth_signature'] ); 564 | $this->normalize(); 565 | $str = $this->serialize(); 566 | $str = strtoupper($http_method).'&'.self::urlencode($http_rsc).'&'.self::urlencode($str); 567 | $key = self::urlencode($this->consumer_secret).'&'.self::urlencode($this->token_secret); 568 | $this->args['oauth_signature'] = base64_encode( hash_hmac( 'sha1', $str, $key, true ) ); 569 | return $this->args; 570 | } 571 | 572 | public function oauth_header(){ 573 | $lines = array(); 574 | foreach( $this->args as $key => $val ){ 575 | $lines[] = self::urlencode($key).'="'.self::urlencode($val).'"'; 576 | } 577 | return 'OAuth '.implode( ",\n ", $lines ); 578 | } 579 | 580 | } 581 | 582 | 583 | 584 | 585 | 586 | 587 | /** 588 | * HTTP status codes with some overridden for Twitter-related messages. 589 | * Note these do not replace error text from Twitter, they're for complete API failures. 590 | * @param int HTTP status code 591 | * @return string HTTP status text 592 | */ 593 | function _twitter_api_http_status_text( $s ){ 594 | static $codes = array ( 595 | 100 => 'Continue', 596 | 101 => 'Switching Protocols', 597 | 598 | 200 => 'OK', 599 | 201 => 'Created', 600 | 202 => 'Accepted', 601 | 203 => 'Non-Authoritative Information', 602 | 204 => 'No Content', 603 | 205 => 'Reset Content', 604 | 206 => 'Partial Content', 605 | 606 | 300 => 'Multiple Choices', 607 | 301 => 'Moved Permanently', 608 | 302 => 'Found', 609 | 303 => 'See Other', 610 | 304 => 'Not Modified', 611 | 305 => 'Use Proxy', 612 | 307 => 'Temporary Redirect', 613 | 614 | 400 => 'Bad Request', 615 | 401 => 'Authorization Required', 616 | 402 => 'Payment Required', 617 | 403 => 'Forbidden', 618 | 404 => 'Not Found', 619 | 405 => 'Method Not Allowed', 620 | 406 => 'Not Acceptable', 621 | 407 => 'Proxy Authentication Required', 622 | 408 => 'Request Time-out', 623 | 409 => 'Conflict', 624 | 410 => 'Gone', 625 | 411 => 'Length Required', 626 | 412 => 'Precondition Failed', 627 | 413 => 'Request Entity Too Large', 628 | 414 => 'Request-URI Too Large', 629 | 415 => 'Unsupported Media Type', 630 | 416 => 'Requested range not satisfiable', 631 | 417 => 'Expectation Failed', 632 | // .. 633 | 429 => 'Twitter API rate limit exceeded', 634 | 635 | 500 => 'Twitter server error', 636 | 501 => 'Not Implemented', 637 | 502 => 'Twitter is not responding', 638 | 503 => 'Twitter is too busy to respond', 639 | 504 => 'Gateway Time-out', 640 | 505 => 'HTTP Version not supported', 641 | ); 642 | return isset($codes[$s]) ? $codes[$s] : sprintf('Status %u from Twitter', $s ); 643 | } 644 | 645 | 646 | 647 | 648 | 649 | /** 650 | * Exception for throwing when Twitter responds with something unpleasant 651 | */ 652 | class TwitterApiException extends Exception { 653 | 654 | /** 655 | * HTTP Status of error 656 | * @var int 657 | */ 658 | protected $status = 0; 659 | 660 | 661 | /** 662 | * Throw appropriate exception type according to HTTP status code 663 | * @param array Twitter error data from their response 664 | */ 665 | public static function chuck( array $err, $status ){ 666 | $code = isset($err['code']) ? (int) $err['code'] : -1; 667 | $mess = isset($err['message']) ? trim($err['message']) : ''; 668 | static $classes = array ( 669 | 404 => 'TwitterApiNotFoundException', 670 | 429 => 'TwitterApiRateLimitException', 671 | ); 672 | $eclass = isset($classes[$status]) ? $classes[$status] : __CLASS__; 673 | throw new $eclass( $mess, $code, $status ); 674 | } 675 | 676 | 677 | /** 678 | * Construct TwitterApiException with addition of HTTP status code. 679 | * @overload 680 | */ 681 | public function __construct( $message, $code = 0 ){ 682 | if( 2 < func_num_args() ){ 683 | $this->status = (int) func_get_arg(2); 684 | } 685 | if( ! $message ){ 686 | $message = _twitter_api_http_status_text($this->status); 687 | } 688 | parent::__construct( $message, $code ); 689 | } 690 | 691 | 692 | /** 693 | * Get HTTP status of error 694 | * @return int 695 | */ 696 | public function getStatus(){ 697 | return $this->status; 698 | } 699 | 700 | } 701 | 702 | 703 | /** 404 */ 704 | class TwitterApiNotFoundException extends TwitterApiException { 705 | 706 | } 707 | 708 | 709 | /** 429 */ 710 | class TwitterApiRateLimitException extends TwitterApiException { 711 | 712 | } 713 | --------------------------------------------------------------------------------