1<?php 2 3/** 4 * @see https://github.com/laminas/laminas-twitter for the canonical source repository 5 * @copyright https://github.com/laminas/laminas-twitter/blob/master/COPYRIGHT.md 6 * @license https://github.com/laminas/laminas-twitter/blob/master/LICENSE.md New BSD License 7 */ 8 9namespace Laminas\Twitter; 10 11use Laminas\Http; 12use Laminas\OAuth as OAuth; 13use Laminas\Stdlib\ArrayUtils; 14use Laminas\Uri; 15use Traversable; 16 17/** 18 * @category Laminas 19 * @package Laminas_Service 20 * @subpackage Twitter 21 */ 22class Twitter 23{ 24 /** 25 * Base URI for all API calls 26 */ 27 const API_BASE_URI = 'https://api.twitter.com/1.1/'; 28 29 /** 30 * OAuth Endpoint 31 */ 32 const OAUTH_BASE_URI = 'https://api.twitter.com/oauth'; 33 34 /** 35 * 246 is the current limit for a status message, 140 characters are displayed 36 * initially, with the remainder linked from the web UI or client. The limit is 37 * applied to a html encoded UTF-8 string (i.e. entities are counted in the limit 38 * which may appear unusual but is a security measure). 39 * 40 * This should be reviewed in the future... 41 */ 42 const STATUS_MAX_CHARACTERS = 246; 43 44 /** 45 * @var array 46 */ 47 protected $cookieJar; 48 49 /** 50 * Date format for 'since' strings 51 * 52 * @var string 53 */ 54 protected $dateFormat = 'D, d M Y H:i:s T'; 55 56 /** 57 * @var Http\Client 58 */ 59 protected $httpClient = null; 60 61 /** 62 * Current method type (for method proxying) 63 * 64 * @var string 65 */ 66 protected $methodType; 67 68 /** 69 * Oauth Consumer 70 * 71 * @var OAuth\Consumer 72 */ 73 protected $oauthConsumer = null; 74 75 /** 76 * Types of API methods 77 * 78 * @var array 79 */ 80 protected $methodTypes = array( 81 'account', 82 'application', 83 'blocks', 84 'directmessages', 85 'favorites', 86 'friendships', 87 'search', 88 'statuses', 89 'users', 90 ); 91 92 /** 93 * Options passed to constructor 94 * 95 * @var array 96 */ 97 protected $options = array(); 98 99 /** 100 * Username 101 * 102 * @var string 103 */ 104 protected $username; 105 106 /** 107 * Constructor 108 * 109 * @param null|array|Traversable $options 110 * @param null|OAuth\Consumer $consumer 111 * @param null|Http\Client $httpClient 112 */ 113 public function __construct($options = null, OAuth\Consumer $consumer = null, Http\Client $httpClient = null) 114 { 115 if ($options instanceof Traversable) { 116 $options = ArrayUtils::iteratorToArray($options); 117 } 118 if (!is_array($options)) { 119 $options = array(); 120 } 121 122 $this->options = $options; 123 124 if (isset($options['username'])) { 125 $this->setUsername($options['username']); 126 } 127 128 $accessToken = false; 129 if (isset($options['accessToken'])) { 130 $accessToken = $options['accessToken']; 131 } elseif (isset($options['access_token'])) { 132 $accessToken = $options['access_token']; 133 } 134 135 $oauthOptions = array(); 136 if (isset($options['oauthOptions'])) { 137 $oauthOptions = $options['oauthOptions']; 138 } elseif (isset($options['oauth_options'])) { 139 $oauthOptions = $options['oauth_options']; 140 } 141 $oauthOptions['siteUrl'] = static::OAUTH_BASE_URI; 142 143 $httpClientOptions = array(); 144 if (isset($options['httpClientOptions'])) { 145 $httpClientOptions = $options['httpClientOptions']; 146 } elseif (isset($options['http_client_options'])) { 147 $httpClientOptions = $options['http_client_options']; 148 } 149 150 // If we have an OAuth access token, use the HTTP client it provides 151 if ($accessToken && is_array($accessToken) 152 && (isset($accessToken['token']) && isset($accessToken['secret'])) 153 ) { 154 $token = new OAuth\Token\Access(); 155 $token->setToken($accessToken['token']); 156 $token->setTokenSecret($accessToken['secret']); 157 $accessToken = $token; 158 } 159 if ($accessToken && $accessToken instanceof OAuth\Token\Access) { 160 $oauthOptions['token'] = $accessToken; 161 $this->setHttpClient($accessToken->getHttpClient($oauthOptions, static::OAUTH_BASE_URI, $httpClientOptions)); 162 return; 163 } 164 165 // See if we were passed an http client 166 if (isset($options['httpClient']) && null === $httpClient) { 167 $httpClient = $options['httpClient']; 168 } elseif (isset($options['http_client']) && null === $httpClient) { 169 $httpClient = $options['http_client']; 170 } 171 if ($httpClient instanceof Http\Client) { 172 $this->httpClient = $httpClient; 173 } else { 174 $this->setHttpClient(new Http\Client(null, $httpClientOptions)); 175 } 176 177 // Set the OAuth consumer 178 if ($consumer === null) { 179 $consumer = new OAuth\Consumer($oauthOptions); 180 } 181 $this->oauthConsumer = $consumer; 182 } 183 184 /** 185 * Proxy service methods 186 * 187 * @param string $type 188 * @return Twitter 189 * @throws Exception\DomainException If method not in method types list 190 */ 191 public function __get($type) 192 { 193 $type = strtolower($type); 194 $type = str_replace('_', '', $type); 195 if (!in_array($type, $this->methodTypes)) { 196 throw new Exception\DomainException( 197 'Invalid method type "' . $type . '"' 198 ); 199 } 200 $this->methodType = $type; 201 return $this; 202 } 203 204 /** 205 * Method overloading 206 * 207 * @param string $method 208 * @param array $params 209 * @return mixed 210 * @throws Exception\BadMethodCallException if unable to find method 211 */ 212 public function __call($method, $params) 213 { 214 if (method_exists($this->oauthConsumer, $method)) { 215 $return = call_user_func_array(array($this->oauthConsumer, $method), $params); 216 if ($return instanceof OAuth\Token\Access) { 217 $this->setHttpClient($return->getHttpClient($this->options)); 218 } 219 return $return; 220 } 221 if (empty($this->methodType)) { 222 throw new Exception\BadMethodCallException( 223 'Invalid method "' . $method . '"' 224 ); 225 } 226 227 $test = str_replace('_', '', strtolower($method)); 228 $test = $this->methodType . $test; 229 if (!method_exists($this, $test)) { 230 throw new Exception\BadMethodCallException( 231 'Invalid method "' . $test . '"' 232 ); 233 } 234 235 return call_user_func_array(array($this, $test), $params); 236 } 237 238 /** 239 * Set HTTP client 240 * 241 * @param Http\Client $client 242 * @return self 243 */ 244 public function setHttpClient(Http\Client $client) 245 { 246 $this->httpClient = $client; 247 $this->httpClient->setHeaders(array('Accept-Charset' => 'ISO-8859-1,utf-8')); 248 return $this; 249 } 250 251 /** 252 * Get the HTTP client 253 * 254 * Lazy loads one if none present 255 * 256 * @return Http\Client 257 */ 258 public function getHttpClient() 259 { 260 if (null === $this->httpClient) { 261 $this->setHttpClient(new Http\Client()); 262 } 263 return $this->httpClient; 264 } 265 266 /** 267 * Retrieve username 268 * 269 * @return string 270 */ 271 public function getUsername() 272 { 273 return $this->username; 274 } 275 276 /** 277 * Set username 278 * 279 * @param string $value 280 * @return self 281 */ 282 public function setUsername($value) 283 { 284 $this->username = $value; 285 return $this; 286 } 287 288 /** 289 * Checks for an authorised state 290 * 291 * @return bool 292 */ 293 public function isAuthorised() 294 { 295 if ($this->getHttpClient() instanceof OAuth\Client) { 296 return true; 297 } 298 return false; 299 } 300 301 /** 302 * Verify Account Credentials 303 * 304 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 305 * @throws Exception\DomainException if unable to decode JSON payload 306 * @return Response 307 */ 308 public function accountVerifyCredentials() 309 { 310 $this->init(); 311 $response = $this->get('account/verify_credentials'); 312 return new Response($response); 313 } 314 315 /** 316 * Returns the number of api requests you have left per hour. 317 * 318 * @todo Have a separate payload object to represent rate limits 319 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 320 * @throws Exception\DomainException if unable to decode JSON payload 321 * @return Response 322 */ 323 public function applicationRateLimitStatus() 324 { 325 $this->init(); 326 $response = $this->get('application/rate_limit_status'); 327 return new Response($response); 328 } 329 330 /** 331 * Blocks the user specified in the ID parameter as the authenticating user. 332 * Destroys a friendship to the blocked user if it exists. 333 * 334 * @param integer|string $id The ID or screen name of a user to block. 335 * @throws Exception\DomainException if unable to decode JSON payload 336 * @return Response 337 */ 338 public function blocksCreate($id) 339 { 340 $this->init(); 341 $path = 'blocks/create'; 342 $params = $this->createUserParameter($id, array()); 343 $response = $this->post($path, $params); 344 return new Response($response); 345 } 346 347 /** 348 * Un-blocks the user specified in the ID parameter for the authenticating user 349 * 350 * @param integer|string $id The ID or screen_name of the user to un-block. 351 * @throws Exception\DomainException if unable to decode JSON payload 352 * @return Response 353 */ 354 public function blocksDestroy($id) 355 { 356 $this->init(); 357 $path = 'blocks/destroy'; 358 $params = $this->createUserParameter($id, array()); 359 $response = $this->post($path, $params); 360 return new Response($response); 361 } 362 363 /** 364 * Returns an array of user ids that the authenticating user is blocking 365 * 366 * @param integer $cursor Optional. Specifies the cursor position at which to begin listing ids; defaults to first "page" of results. 367 * @throws Exception\DomainException if unable to decode JSON payload 368 * @return Response 369 */ 370 public function blocksIds($cursor = -1) 371 { 372 $this->init(); 373 $path = 'blocks/ids'; 374 $response = $this->get($path, array('cursor' => $cursor)); 375 return new Response($response); 376 } 377 378 /** 379 * Returns an array of user objects that the authenticating user is blocking 380 * 381 * @param integer $cursor Optional. Specifies the cursor position at which to begin listing ids; defaults to first "page" of results. 382 * @throws Exception\DomainException if unable to decode JSON payload 383 * @return Response 384 */ 385 public function blocksList($cursor = -1) 386 { 387 $this->init(); 388 $path = 'blocks/list'; 389 $response = $this->get($path, array('cursor' => $cursor)); 390 return new Response($response); 391 } 392 393 /** 394 * Destroy a direct message 395 * 396 * @param int $id ID of message to destroy 397 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 398 * @throws Exception\DomainException if unable to decode JSON payload 399 * @return Response 400 */ 401 public function directMessagesDestroy($id) 402 { 403 $this->init(); 404 $path = 'direct_messages/destroy'; 405 $params = array('id' => $this->validInteger($id)); 406 $response = $this->post($path, $params); 407 return new Response($response); 408 } 409 410 /** 411 * Retrieve direct messages for the current user 412 * 413 * $options may include one or more of the following keys 414 * - count: return page X of results 415 * - since_id: return statuses only greater than the one specified 416 * - max_id: return statuses with an ID less than (older than) or equal to that specified 417 * - include_entities: setting to false will disable embedded entities 418 * - skip_status:setting to true, "t", or 1 will omit the status in returned users 419 * 420 * @param array $options 421 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 422 * @throws Exception\DomainException if unable to decode JSON payload 423 * @return Response 424 */ 425 public function directMessagesMessages(array $options = array()) 426 { 427 $this->init(); 428 $path = 'direct_messages'; 429 $params = array(); 430 foreach ($options as $key => $value) { 431 switch (strtolower($key)) { 432 case 'count': 433 $params['count'] = (int) $value; 434 break; 435 case 'since_id': 436 $params['since_id'] = $this->validInteger($value); 437 break; 438 case 'max_id': 439 $params['max_id'] = $this->validInteger($value); 440 break; 441 case 'include_entities': 442 $params['include_entities'] = (bool) $value; 443 break; 444 case 'skip_status': 445 $params['skip_status'] = (bool) $value; 446 break; 447 default: 448 break; 449 } 450 } 451 $response = $this->get($path, $params); 452 return new Response($response); 453 } 454 455 /** 456 * Send a direct message to a user 457 * 458 * @param int|string $user User to whom to send message 459 * @param string $text Message to send to user 460 * @throws Exception\InvalidArgumentException if message is empty 461 * @throws Exception\OutOfRangeException if message is too long 462 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 463 * @throws Exception\DomainException if unable to decode JSON payload 464 * @return Response 465 */ 466 public function directMessagesNew($user, $text) 467 { 468 $this->init(); 469 $path = 'direct_messages/new'; 470 471 $len = iconv_strlen($text, 'UTF-8'); 472 if (0 == $len) { 473 throw new Exception\InvalidArgumentException( 474 'Direct message must contain at least one character' 475 ); 476 } elseif (140 < $len) { 477 throw new Exception\OutOfRangeException( 478 'Direct message must contain no more than 140 characters' 479 ); 480 } 481 482 $params = $this->createUserParameter($user, array()); 483 $params['text'] = $text; 484 $response = $this->post($path, $params); 485 return new Response($response); 486 } 487 488 /** 489 * Retrieve list of direct messages sent by current user 490 * 491 * $options may include one or more of the following keys 492 * - count: return page X of results 493 * - page: return starting at page 494 * - since_id: return statuses only greater than the one specified 495 * - max_id: return statuses with an ID less than (older than) or equal to that specified 496 * - include_entities: setting to false will disable embedded entities 497 * 498 * @param array $options 499 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 500 * @throws Exception\DomainException if unable to decode JSON payload 501 * @return Response 502 */ 503 public function directMessagesSent(array $options = array()) 504 { 505 $this->init(); 506 $path = 'direct_messages/sent'; 507 $params = array(); 508 foreach ($options as $key => $value) { 509 switch (strtolower($key)) { 510 case 'count': 511 $params['count'] = (int) $value; 512 break; 513 case 'page': 514 $params['page'] = (int) $value; 515 break; 516 case 'since_id': 517 $params['since_id'] = $this->validInteger($value); 518 break; 519 case 'max_id': 520 $params['max_id'] = $this->validInteger($value); 521 break; 522 case 'include_entities': 523 $params['include_entities'] = (bool) $value; 524 break; 525 default: 526 break; 527 } 528 } 529 $response = $this->get($path, $params); 530 return new Response($response); 531 } 532 533 /** 534 * Mark a status as a favorite 535 * 536 * @param int $id Status ID you want to mark as a favorite 537 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 538 * @throws Exception\DomainException if unable to decode JSON payload 539 * @return Response 540 */ 541 public function favoritesCreate($id) 542 { 543 $this->init(); 544 $path = 'favorites/create'; 545 $params = array('id' => $this->validInteger($id)); 546 $response = $this->post($path, $params); 547 return new Response($response); 548 } 549 550 /** 551 * Remove a favorite 552 * 553 * @param int $id Status ID you want to de-list as a favorite 554 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 555 * @throws Exception\DomainException if unable to decode JSON payload 556 * @return Response 557 */ 558 public function favoritesDestroy($id) 559 { 560 $this->init(); 561 $path = 'favorites/destroy'; 562 $params = array('id' => $this->validInteger($id)); 563 $response = $this->post($path, $params); 564 return new Response($response); 565 } 566 567 /** 568 * Fetch favorites 569 * 570 * $options may contain one or more of the following: 571 * - user_id: Id of a user for whom to fetch favorites 572 * - screen_name: Screen name of a user for whom to fetch favorites 573 * - count: number of tweets to attempt to retrieve, up to 200 574 * - since_id: return results only after the specified tweet id 575 * - max_id: return results with an ID less than (older than) or equal to the specified ID 576 * - include_entities: when set to false, entities member will be omitted 577 * 578 * @param array $params 579 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 580 * @throws Exception\DomainException if unable to decode JSON payload 581 * @return Response 582 */ 583 public function favoritesList(array $options = array()) 584 { 585 $this->init(); 586 $path = 'favorites/list'; 587 $params = array(); 588 foreach ($options as $key => $value) { 589 switch (strtolower($key)) { 590 case 'user_id': 591 $params['user_id'] = $this->validInteger($value); 592 break; 593 case 'screen_name': 594 $params['screen_name'] = $value; 595 break; 596 case 'count': 597 $params['count'] = (int) $value; 598 break; 599 case 'since_id': 600 $params['since_id'] = $this->validInteger($value); 601 break; 602 case 'max_id': 603 $params['max_id'] = $this->validInteger($value); 604 break; 605 case 'include_entities': 606 $params['include_entities'] = (bool) $value; 607 break; 608 default: 609 break; 610 } 611 } 612 $response = $this->get($path, $params); 613 return new Response($response); 614 } 615 616 /** 617 * Create friendship 618 * 619 * @param int|string $id User ID or name of new friend 620 * @param array $params Additional parameters to pass 621 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 622 * @throws Exception\DomainException if unable to decode JSON payload 623 * @return Response 624 */ 625 public function friendshipsCreate($id, array $params = array()) 626 { 627 $this->init(); 628 $path = 'friendships/create'; 629 $params = $this->createUserParameter($id, $params); 630 $allowed = array( 631 'user_id' => null, 632 'screen_name' => null, 633 'follow' => null, 634 ); 635 $params = array_intersect_key($params, $allowed); 636 $response = $this->post($path, $params); 637 return new Response($response); 638 } 639 640 /** 641 * Destroy friendship 642 * 643 * @param int|string $id User ID or name of friend to remove 644 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 645 * @throws Exception\DomainException if unable to decode JSON payload 646 * @return Response 647 */ 648 public function friendshipsDestroy($id) 649 { 650 $this->init(); 651 $path = 'friendships/destroy'; 652 $params = $this->createUserParameter($id, array()); 653 $response = $this->post($path, $params); 654 return new Response($response); 655 } 656 657 /** 658 * Search tweets 659 * 660 * $options may include any of the following: 661 * - geocode: a string of the form "latitude, longitude, radius" 662 * - lang: restrict tweets to the two-letter language code 663 * - locale: query is in the given two-letter language code 664 * - result_type: what type of results to receive: mixed, recent, or popular 665 * - count: number of tweets to return per page; up to 100 666 * - until: return tweets generated before the given date 667 * - since_id: return resutls with an ID greater than (more recent than) the given ID 668 * - max_id: return results with an ID less than (older than) the given ID 669 * - include_entities: whether or not to include embedded entities 670 * 671 * @param string $query 672 * @param array $options 673 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 674 * @throws Exception\DomainException if unable to decode JSON payload 675 * @return Response 676 */ 677 public function searchTweets($query, array $options = array()) 678 { 679 $this->init(); 680 $path = 'search/tweets'; 681 682 $len = iconv_strlen($query, 'UTF-8'); 683 if (0 == $len) { 684 throw new Exception\InvalidArgumentException( 685 'Query must contain at least one character' 686 ); 687 } 688 689 $params = array('q' => $query); 690 foreach ($options as $key => $value) { 691 switch (strtolower($key)) { 692 case 'geocode': 693 if (!substr_count($value, ',') !== 2) { 694 throw new Exception\InvalidArgumentException( 695 '"geocode" must be of the format "latitude,longitude,radius"' 696 ); 697 } 698 list($latitude, $longitude, $radius) = explode(',', $value); 699 $radius = trim($radius); 700 if (!preg_match('/^\d+(mi|km)$/', $radius)) { 701 throw new Exception\InvalidArgumentException( 702 'Radius segment of "geocode" must be of the format "[unit](mi|km)"' 703 ); 704 } 705 $latitude = (float) $latitude; 706 $longitude = (float) $longitude; 707 $params['geocode'] = $latitude . ',' . $longitude . ',' . $radius; 708 break; 709 case 'lang': 710 if (strlen($value) > 2) { 711 throw new Exception\InvalidArgumentException( 712 'Query language must be a 2 character string' 713 ); 714 } 715 $params['lang'] = strtolower($value); 716 break; 717 case 'locale': 718 if (strlen($value) > 2) { 719 throw new Exception\InvalidArgumentException( 720 'Query locale must be a 2 character string' 721 ); 722 } 723 $params['locale'] = strtolower($value); 724 break; 725 case 'result_type': 726 $value = strtolower($value); 727 if (!in_array($value, array('mixed', 'recent', 'popular'))) { 728 throw new Exception\InvalidArgumentException( 729 'result_type must be one of "mixed", "recent", or "popular"' 730 ); 731 } 732 $params['result_type'] = $value; 733 break; 734 case 'count': 735 $value = (int) $value; 736 if (1 > $value || 100 < $value) { 737 throw new Exception\InvalidArgumentException( 738 'count must be between 1 and 100' 739 ); 740 } 741 $params['count'] = $value; 742 break; 743 case 'until': 744 if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { 745 throw new Exception\InvalidArgumentException( 746 '"until" must be a date in the format YYYY-MM-DD' 747 ); 748 } 749 $params['until'] = $value; 750 break; 751 case 'since_id': 752 $params['since_id'] = $this->validInteger($value); 753 break; 754 case 'max_id': 755 $params['max_id'] = $this->validInteger($value); 756 break; 757 case 'include_entities': 758 $params['include_entities'] = (bool) $value; 759 break; 760 default: 761 break; 762 } 763 } 764 $response = $this->get($path, $params); 765 return new Response($response); 766 } 767 768 /** 769 * Destroy a status message 770 * 771 * @param int $id ID of status to destroy 772 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 773 * @throws Exception\DomainException if unable to decode JSON payload 774 * @return Response 775 */ 776 public function statusesDestroy($id) 777 { 778 $this->init(); 779 $path = 'statuses/destroy/' . $this->validInteger($id); 780 $response = $this->post($path); 781 return new Response($response); 782 } 783 784 /** 785 * Friend Timeline Status 786 * 787 * $options may include one or more of the following keys 788 * - count: number of tweets to attempt to retrieve, up to 200 789 * - since_id: return results only after the specified tweet id 790 * - max_id: return results with an ID less than (older than) or equal to the specified ID 791 * - trim_user: when set to true, "t", or 1, user object in tweets will include only author's ID. 792 * - contributor_details: when set to true, includes screen_name of each contributor 793 * - include_entities: when set to false, entities member will be omitted 794 * - exclude_replies: when set to true, will strip replies appearing in the timeline 795 * 796 * @param array $params 797 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 798 * @throws Exception\DomainException if unable to decode JSON payload 799 * @return Response 800 */ 801 public function statusesHomeTimeline(array $options = array()) 802 { 803 $this->init(); 804 $path = 'statuses/home_timeline'; 805 $params = array(); 806 foreach ($options as $key => $value) { 807 switch (strtolower($key)) { 808 case 'count': 809 $params['count'] = (int) $value; 810 break; 811 case 'since_id': 812 $params['since_id'] = $this->validInteger($value); 813 break; 814 case 'max_id': 815 $params['max_id'] = $this->validInteger($value); 816 break; 817 case 'trim_user': 818 if (in_array($value, array(true, 'true', 't', 1, '1'))) { 819 $value = true; 820 } else { 821 $value = false; 822 } 823 $params['trim_user'] = $value; 824 break; 825 case 'contributor_details:': 826 $params['contributor_details:'] = (bool) $value; 827 break; 828 case 'include_entities': 829 $params['include_entities'] = (bool) $value; 830 break; 831 case 'exclude_replies': 832 $params['exclude_replies'] = (bool) $value; 833 break; 834 default: 835 break; 836 } 837 } 838 $response = $this->get($path, $params); 839 return new Response($response); 840 } 841 842 /** 843 * Get status replies 844 * 845 * $options may include one or more of the following keys 846 * - count: number of tweets to attempt to retrieve, up to 200 847 * - since_id: return results only after the specified tweet id 848 * - max_id: return results with an ID less than (older than) or equal to the specified ID 849 * - trim_user: when set to true, "t", or 1, user object in tweets will include only author's ID. 850 * - contributor_details: when set to true, includes screen_name of each contributor 851 * - include_entities: when set to false, entities member will be omitted 852 * 853 * @param array $options 854 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 855 * @throws Exception\DomainException if unable to decode JSON payload 856 * @return Response 857 */ 858 public function statusesMentionsTimeline(array $options = array()) 859 { 860 $this->init(); 861 $path = 'statuses/mentions_timeline'; 862 $params = array(); 863 foreach ($options as $key => $value) { 864 switch (strtolower($key)) { 865 case 'count': 866 $params['count'] = (int) $value; 867 break; 868 case 'since_id': 869 $params['since_id'] = $this->validInteger($value); 870 break; 871 case 'max_id': 872 $params['max_id'] = $this->validInteger($value); 873 break; 874 case 'trim_user': 875 if (in_array($value, array(true, 'true', 't', 1, '1'))) { 876 $value = true; 877 } else { 878 $value = false; 879 } 880 $params['trim_user'] = $value; 881 break; 882 case 'contributor_details:': 883 $params['contributor_details:'] = (bool) $value; 884 break; 885 case 'include_entities': 886 $params['include_entities'] = (bool) $value; 887 break; 888 default: 889 break; 890 } 891 } 892 $response = $this->get($path, $params); 893 return new Response($response); 894 } 895 896 /** 897 * Public Timeline status 898 * 899 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 900 * @throws Exception\DomainException if unable to decode JSON payload 901 * @return Response 902 */ 903 public function statusesSample() 904 { 905 $this->init(); 906 $path = 'statuses/sample'; 907 $response = $this->get($path); 908 return new Response($response); 909 } 910 911 /** 912 * Show a single status 913 * 914 * @param int $id Id of status to show 915 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 916 * @throws Exception\DomainException if unable to decode JSON payload 917 * @return Response 918 */ 919 public function statusesShow($id) 920 { 921 $this->init(); 922 $path = 'statuses/show/' . $this->validInteger($id); 923 $response = $this->get($path); 924 return new Response($response); 925 } 926 927 /** 928 * Update user's current status 929 * 930 * @todo Support additional parameters supported by statuses/update endpoint 931 * @param string $status 932 * @param null|int $inReplyToStatusId 933 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 934 * @throws Exception\OutOfRangeException if message is too long 935 * @throws Exception\InvalidArgumentException if message is empty 936 * @throws Exception\DomainException if unable to decode JSON payload 937 * @return Response 938 */ 939 public function statusesUpdate($status, $inReplyToStatusId = null) 940 { 941 $this->init(); 942 $path = 'statuses/update'; 943 $len = iconv_strlen(htmlspecialchars($status, ENT_QUOTES, 'UTF-8'), 'UTF-8'); 944 if ($len > self::STATUS_MAX_CHARACTERS) { 945 throw new Exception\OutOfRangeException( 946 'Status must be no more than ' 947 . self::STATUS_MAX_CHARACTERS 948 . ' characters in length' 949 ); 950 } elseif (0 == $len) { 951 throw new Exception\InvalidArgumentException( 952 'Status must contain at least one character' 953 ); 954 } 955 956 $params = array('status' => $status); 957 $inReplyToStatusId = $this->validInteger($inReplyToStatusId); 958 if ($inReplyToStatusId) { 959 $params['in_reply_to_status_id'] = $inReplyToStatusId; 960 } 961 $response = $this->post($path, $params); 962 return new Response($response); 963 } 964 965 /** 966 * User Timeline status 967 * 968 * $options may include one or more of the following keys 969 * - user_id: Id of a user for whom to fetch favorites 970 * - screen_name: Screen name of a user for whom to fetch favorites 971 * - count: number of tweets to attempt to retrieve, up to 200 972 * - since_id: return results only after the specified tweet id 973 * - max_id: return results with an ID less than (older than) or equal to the specified ID 974 * - trim_user: when set to true, "t", or 1, user object in tweets will include only author's ID. 975 * - exclude_replies: when set to true, will strip replies appearing in the timeline 976 * - contributor_details: when set to true, includes screen_name of each contributor 977 * - include_rts: when set to false, will strip native retweets 978 * 979 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 980 * @throws Exception\DomainException if unable to decode JSON payload 981 * @return Response 982 */ 983 public function statusesUserTimeline(array $options = array()) 984 { 985 $this->init(); 986 $path = 'statuses/user_timeline'; 987 $params = array(); 988 foreach ($options as $key => $value) { 989 switch (strtolower($key)) { 990 case 'user_id': 991 $params['user_id'] = $this->validInteger($value); 992 break; 993 case 'screen_name': 994 $params['screen_name'] = $this->validateScreenName($value); 995 break; 996 case 'count': 997 $params['count'] = (int) $value; 998 break; 999 case 'since_id': 1000 $params['since_id'] = $this->validInteger($value); 1001 break; 1002 case 'max_id': 1003 $params['max_id'] = $this->validInteger($value); 1004 break; 1005 case 'trim_user': 1006 if (in_array($value, array(true, 'true', 't', 1, '1'))) { 1007 $value = true; 1008 } else { 1009 $value = false; 1010 } 1011 $params['trim_user'] = $value; 1012 break; 1013 case 'contributor_details:': 1014 $params['contributor_details:'] = (bool) $value; 1015 break; 1016 case 'exclude_replies': 1017 $params['exclude_replies'] = (bool) $value; 1018 break; 1019 case 'include_rts': 1020 $params['include_rts'] = (bool) $value; 1021 break; 1022 default: 1023 break; 1024 } 1025 } 1026 $response = $this->get($path, $params); 1027 return new Response($response); 1028 } 1029 1030 /** 1031 * Search users 1032 * 1033 * $options may include any of the following: 1034 * - page: the page of results to retrieve 1035 * - count: the number of users to retrieve per page; max is 20 1036 * - include_entities: if set to boolean true, include embedded entities 1037 * 1038 * @param string $query 1039 * @param array $options 1040 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 1041 * @throws Exception\DomainException if unable to decode JSON payload 1042 * @return Response 1043 */ 1044 public function usersSearch($query, array $options = array()) 1045 { 1046 $this->init(); 1047 $path = 'users/search'; 1048 1049 $len = iconv_strlen($query, 'UTF-8'); 1050 if (0 == $len) { 1051 throw new Exception\InvalidArgumentException( 1052 'Query must contain at least one character' 1053 ); 1054 } 1055 1056 $params = array('q' => $query); 1057 foreach ($options as $key => $value) { 1058 switch (strtolower($key)) { 1059 case 'count': 1060 $value = (int) $value; 1061 if (1 > $value || 20 < $value) { 1062 throw new Exception\InvalidArgumentException( 1063 'count must be between 1 and 20' 1064 ); 1065 } 1066 $params['count'] = $value; 1067 break; 1068 case 'page': 1069 $params['page'] = (int) $value; 1070 break; 1071 case 'include_entities': 1072 $params['include_entities'] = (bool) $value; 1073 break; 1074 default: 1075 break; 1076 } 1077 } 1078 $response = $this->get($path, $params); 1079 return new Response($response); 1080 } 1081 1082 1083 /** 1084 * Show extended information on a user 1085 * 1086 * @param int|string $id User ID or name 1087 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 1088 * @throws Exception\DomainException if unable to decode JSON payload 1089 * @return Response 1090 */ 1091 public function usersShow($id) 1092 { 1093 $this->init(); 1094 $path = 'users/show'; 1095 $params = $this->createUserParameter($id, array()); 1096 $response = $this->get($path, $params); 1097 return new Response($response); 1098 } 1099 1100 /** 1101 * Initialize HTTP authentication 1102 * 1103 * @return void 1104 * @throws Exception\DomainException if unauthorised 1105 */ 1106 protected function init() 1107 { 1108 if (!$this->isAuthorised() && $this->getUsername() !== null) { 1109 throw new Exception\DomainException( 1110 'Twitter session is unauthorised. You need to initialize ' 1111 . __CLASS__ . ' with an OAuth Access Token or use ' 1112 . 'its OAuth functionality to obtain an Access Token before ' 1113 . 'attempting any API actions that require authorisation' 1114 ); 1115 } 1116 $client = $this->getHttpClient(); 1117 $client->resetParameters(); 1118 if (null === $this->cookieJar) { 1119 $client->clearCookies(); 1120 $this->cookieJar = $client->getCookies(); 1121 } else { 1122 $client->setCookies($this->cookieJar); 1123 } 1124 } 1125 1126 /** 1127 * Protected function to validate that the integer is valid or return a 0 1128 * 1129 * @param $int 1130 * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out 1131 * @return integer 1132 */ 1133 protected function validInteger($int) 1134 { 1135 if (preg_match("/(\d+)/", $int)) { 1136 return $int; 1137 } 1138 return 0; 1139 } 1140 1141 /** 1142 * Validate a screen name using Twitter rules 1143 * 1144 * @param string $name 1145 * @return string 1146 * @throws Exception\InvalidArgumentException 1147 */ 1148 protected function validateScreenName($name) 1149 { 1150 if (!preg_match('/^[a-zA-Z0-9_]{0,20}$/', $name)) { 1151 throw new Exception\InvalidArgumentException( 1152 'Screen name, "' . $name 1153 . '" should only contain alphanumeric characters and' 1154 . ' underscores, and not exceed 15 characters.'); 1155 } 1156 return $name; 1157 } 1158 1159 /** 1160 * Call a remote REST web service URI 1161 * 1162 * @param string $path The path to append to the URI 1163 * @param Http\Client $client 1164 * @throws Client\Exception\UnexpectedValueException 1165 * @return void 1166 */ 1167 protected function prepare($path, Http\Client $client) 1168 { 1169 $client->setUri(static::API_BASE_URI . $path . '.json'); 1170 1171 /** 1172 * Do this each time to ensure oauth calls do not inject new params 1173 */ 1174 $client->resetParameters(); 1175 } 1176 1177 /** 1178 * Performs an HTTP GET request to the $path. 1179 * 1180 * @param string $path 1181 * @param array $query Array of GET parameters 1182 * @throws Http\Client\Exception\ExceptionInterface 1183 * @return Http\Response 1184 */ 1185 protected function get($path, array $query = array()) 1186 { 1187 $client = $this->getHttpClient(); 1188 $this->prepare($path, $client); 1189 $client->setParameterGet($query); 1190 $client->setMethod(Http\Request::METHOD_GET); 1191 $response = $client->send(); 1192 return $response; 1193 } 1194 1195 /** 1196 * Performs an HTTP POST request to $path. 1197 * 1198 * @param string $path 1199 * @param mixed $data Raw data to send 1200 * @throws Http\Client\Exception\ExceptionInterface 1201 * @return Http\Response 1202 */ 1203 protected function post($path, $data = null) 1204 { 1205 $client = $this->getHttpClient(); 1206 $this->prepare($path, $client); 1207 return $this->performPost(Http\Request::METHOD_POST, $data, $client); 1208 } 1209 1210 /** 1211 * Perform a POST or PUT 1212 * 1213 * Performs a POST or PUT request. Any data provided is set in the HTTP 1214 * client. String data is pushed in as raw POST data; array or object data 1215 * is pushed in as POST parameters. 1216 * 1217 * @param mixed $method 1218 * @param mixed $data 1219 * @return Http\Response 1220 */ 1221 protected function performPost($method, $data, Http\Client $client) 1222 { 1223 if (is_string($data)) { 1224 $client->setRawData($data); 1225 } elseif (is_array($data) || is_object($data)) { 1226 $client->setParameterPost((array) $data); 1227 } 1228 $client->setMethod($method); 1229 return $client->send(); 1230 } 1231 1232 /** 1233 * Create a parameter representing the user 1234 * 1235 * Determines if $id is an integer, and, if so, sets the "user_id" parameter. 1236 * If not, assumes the $id is the "screen_name". 1237 * 1238 * @param int|string $id 1239 * @param array $params 1240 * @return array 1241 */ 1242 protected function createUserParameter($id, array $params) 1243 { 1244 if ($this->validInteger($id)) { 1245 $params['user_id'] = $id; 1246 return $params; 1247 } 1248 1249 $params['screen_name'] = $this->validateScreenName($id); 1250 return $params; 1251 } 1252} 1253