Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| <?php | |
| /** | |
| * @see https://github.com/zendframework/ZendService_Twitter for the canonical source repository | |
| * @copyright Copyright (c) 2005-2017 Zend Technologies USA Inc. (http://www.zend.com) | |
| * @license https://github.com/zendframework/ZendService_Twitter/blob/master/LICENSE.md New BSD License | |
| */ | |
| namespace ZendService\Twitter; | |
| use Closure; | |
| use Normalizer; | |
| use Traversable; | |
| use ZendOAuth as OAuth; | |
| use Zend\Http; | |
| use Zend\Stdlib\ArrayUtils; | |
| use Zend\Uri; | |
| /** | |
| * Interact with the Twitter API. | |
| * | |
| * Note: most `$id` parameters accept either string or integer values. This is | |
| * due to the fact that identifiers in the Twitter API may exceed PHP_INT_MAX. | |
| * | |
| * Note on character counting: Twitter accepts UTF-8 encoded text via the API, | |
| * and counts multi-byte characters as a single character. PHP's strlen(), | |
| * however, treats each byte as a character for purposes of determing the | |
| * string length. To get around that, we can pass the message to utf8_decode, | |
| * which will replace any multi-byte characters with a `?`; this works fine | |
| * for counting lengths. | |
| * | |
| * @see https://developer.twitter.com/en/docs/basics/counting-characters | |
| */ | |
| class Twitter | |
| { | |
| /** | |
| * Base URI for all API calls | |
| */ | |
| const API_BASE_URI = 'https://api.twitter.com/1.1/'; | |
| /** | |
| * OAuth Endpoint | |
| */ | |
| const OAUTH_BASE_URI = 'https://api.twitter.com/oauth'; | |
| /** | |
| * Paths that use JSON payloads (vs form-encoded) | |
| */ | |
| const PATHS_JSON_PAYLOAD = [ | |
| 'direct_messages/events/new', | |
| 'direct_messages/welcome_messages/new', | |
| 'direct_messages/welcome_messages/rules/new', | |
| ]; | |
| /** | |
| * As of November 2017, the character limit for status messages is 280. | |
| */ | |
| const STATUS_MAX_CHARACTERS = 280; | |
| /** | |
| * @var array | |
| */ | |
| private $cookieJar; | |
| /** | |
| * Date format for 'since' strings | |
| * | |
| * @var string | |
| */ | |
| private $dateFormat = 'D, d M Y H:i:s T'; | |
| /** | |
| * @var Http\Client|null | |
| */ | |
| private $httpClient; | |
| /** | |
| * Flags to use with json_encode for POST requests | |
| * | |
| * @var int | |
| */ | |
| private $jsonFlags = JSON_PRESERVE_ZERO_FRACTION | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; | |
| /** | |
| * Current method type (for method proxying) | |
| * | |
| * @var string|null | |
| */ | |
| private $methodType; | |
| /** | |
| * Types of API methods | |
| * | |
| * @var array | |
| */ | |
| private $methodTypes = [ | |
| 'account', | |
| 'application', | |
| 'blocks', | |
| 'directmessages', | |
| 'favorites', | |
| 'followers', | |
| 'friends', | |
| 'friendships', | |
| 'lists', | |
| 'search', | |
| 'statuses', | |
| 'users', | |
| ]; | |
| /** | |
| * Oauth Consumer | |
| * | |
| * @var OAuth\Consumer|null | |
| */ | |
| private $oauthConsumer; | |
| /** | |
| * Options passed to constructor | |
| * | |
| * @var array | |
| */ | |
| private $options = []; | |
| /** | |
| * Username | |
| * | |
| * @var string | |
| */ | |
| private $username; | |
| public function __construct( | |
| iterable $options = null, | |
| OAuth\Consumer $consumer = null, | |
| Http\Client $httpClient = null | |
| ) { | |
| if ($options instanceof Traversable) { | |
| $options = ArrayUtils::iteratorToArray($options); | |
| } | |
| if (! is_array($options)) { | |
| $options = []; | |
| } | |
| $this->options = $options; | |
| if (isset($options['username'])) { | |
| $this->setUsername($options['username']); | |
| } | |
| $accessToken = false; | |
| if (isset($options['accessToken'])) { | |
| $accessToken = $options['accessToken']; | |
| } elseif (isset($options['access_token'])) { | |
| $accessToken = $options['access_token']; | |
| } | |
| $oauthOptions = []; | |
| if (isset($options['oauthOptions'])) { | |
| $oauthOptions = $options['oauthOptions']; | |
| } elseif (isset($options['oauth_options'])) { | |
| $oauthOptions = $options['oauth_options']; | |
| } | |
| $oauthOptions['siteUrl'] = static::OAUTH_BASE_URI; | |
| $httpClientOptions = []; | |
| if (isset($options['httpClientOptions'])) { | |
| $httpClientOptions = $options['httpClientOptions']; | |
| } elseif (isset($options['http_client_options'])) { | |
| $httpClientOptions = $options['http_client_options']; | |
| } | |
| // If we have an OAuth access token, use the HTTP client it provides | |
| if ($accessToken && is_array($accessToken) | |
| && (isset($accessToken['token']) && isset($accessToken['secret'])) | |
| ) { | |
| $token = new OAuth\Token\Access(); | |
| $token->setToken($accessToken['token']); | |
| $token->setTokenSecret($accessToken['secret']); | |
| $accessToken = $token; | |
| } | |
| if ($accessToken && $accessToken instanceof OAuth\Token\Access) { | |
| $oauthOptions['token'] = $accessToken; | |
| $this->setHttpClient($accessToken->getHttpClient( | |
| $oauthOptions, | |
| static::OAUTH_BASE_URI, | |
| $httpClientOptions | |
| )); | |
| return; | |
| } | |
| // See if we were passed an http client | |
| if (isset($options['httpClient']) && null === $httpClient) { | |
| $httpClient = $options['httpClient']; | |
| } elseif (isset($options['http_client']) && null === $httpClient) { | |
| $httpClient = $options['http_client']; | |
| } | |
| if ($httpClient instanceof Http\Client) { | |
| $this->httpClient = $httpClient; | |
| } else { | |
| $this->setHttpClient(new Http\Client(null, $httpClientOptions)); | |
| } | |
| // Set the OAuth consumer | |
| if ($consumer === null) { | |
| $consumer = new OAuth\Consumer($oauthOptions); | |
| } | |
| $this->oauthConsumer = $consumer; | |
| } | |
| /** | |
| * Proxy service methods. | |
| * | |
| * Allows performing method proxy calls via property access; see {@link __call} | |
| * for more details. | |
| * | |
| * Valid `$type` values currently include: | |
| * | |
| * - account | |
| * - application | |
| * - blocks | |
| * - directmessages | |
| * - favorites | |
| * - followers | |
| * - friends | |
| * - friendships | |
| * - lists | |
| * - search | |
| * - statuses | |
| * - users | |
| * | |
| * @throws Exception\DomainException If method not in method types list | |
| */ | |
| public function __get(string $type) : self | |
| { | |
| $type = strtolower($type); | |
| $type = str_replace('_', '', $type); | |
| if (! in_array($type, $this->methodTypes)) { | |
| throw new Exception\DomainException( | |
| 'Invalid method type "' . $type . '"' | |
| ); | |
| } | |
| $this->methodType = $type; | |
| return $this; | |
| } | |
| /** | |
| * Proxy method calls. | |
| * | |
| * Acts as a method proxy in two ways. | |
| * | |
| * First, if the method exists on the `$oauthConsumer` property, it will call | |
| * it using the provided parameters. | |
| * | |
| * Second, it will proxy to specific Twitter API segments. If the user requests | |
| * a property of this class that maps to a known method type ({@link $methodTypes}), | |
| * that value is stored in `$methodType` and the current instance is returned; | |
| * method overloading then checks to see if `$methodType` + `$method` is a known | |
| * method of this class, and, if so, calls it. (Underscores are stripped in both | |
| * processes.) | |
| * | |
| * As examples: | |
| * | |
| * <code> | |
| * $response = $twitter->search->tweets('#php'); | |
| * $response = $twitter->users->search('zfdevteam'); | |
| * $response = $twitter->users->search('zfdevteam'); | |
| * $response = $twitter->statuses->mentions_timeline('zfdevteam'); | |
| * </code> | |
| * | |
| * @return mixed | |
| * @throws Exception\BadMethodCallException if unable to find method | |
| */ | |
| public function __call(string $method, array $params) | |
| { | |
| if (method_exists($this->oauthConsumer, $method)) { | |
| $return = $this->oauthConsumer->$method(...$params); | |
| if ($return instanceof OAuth\Token\Access) { | |
| $this->setHttpClient($return->getHttpClient($this->options)); | |
| } | |
| return $return; | |
| } | |
| if (empty($this->methodType)) { | |
| throw new Exception\BadMethodCallException( | |
| 'Invalid method "' . $method . '"' | |
| ); | |
| } | |
| $test = str_replace('_', '', strtolower($method)); | |
| $test = $this->methodType . $test; | |
| if (! method_exists($this, $test)) { | |
| throw new Exception\BadMethodCallException( | |
| 'Invalid method "' . $test . '"' | |
| ); | |
| } | |
| return $this->$test(...$params); | |
| } | |
| /** | |
| * Set HTTP client. | |
| */ | |
| public function setHttpClient(Http\Client $client) : self | |
| { | |
| $this->httpClient = $client; | |
| $this->httpClient->setHeaders(['Accept-Charset' => 'ISO-8859-1,utf-8']); | |
| return $this; | |
| } | |
| /** | |
| * Get the HTTP client. | |
| * | |
| * Lazy loads one if none present. | |
| */ | |
| public function getHttpClient() : Http\Client | |
| { | |
| if (null === $this->httpClient) { | |
| $this->setHttpClient(new Http\Client()); | |
| } | |
| return $this->httpClient; | |
| } | |
| /** | |
| * Retrieve username. | |
| */ | |
| public function getUsername() : ?string | |
| { | |
| return $this->username; | |
| } | |
| /** | |
| * Set username. | |
| */ | |
| public function setUsername(string $value) : self | |
| { | |
| $this->username = $value; | |
| return $this; | |
| } | |
| /** | |
| * Checks for an authorised state. | |
| */ | |
| public function isAuthorised(Http\Client $client = null) : bool | |
| { | |
| $client = $client ?: $this->getHttpClient(); | |
| if ($client instanceof OAuth\Client) { | |
| return true; | |
| } | |
| return false; | |
| } | |
| /** | |
| * Performs an HTTP GET request to the $path. | |
| * | |
| * Used internally for all Twitter API GET calls, this method returns a | |
| * Response instance from a request made to API_BASE_URL + $path + '.json'; | |
| * if any $query parameters are provided, they are appended as the query | |
| * string when the request is made. | |
| * | |
| * Call this method if you wish to make a GET request to an endpoint that | |
| * does not yet have a corresponding method in this class. As an example: | |
| * | |
| * <code> | |
| * $response = $twitter->get('friends/list', ['screen_name' => 'zfdevteam']); | |
| * foreach ($response->users as $user) { | |
| * printf("- %s (%s)\n", $user->name, $user->screen_name); | |
| * } | |
| * </code> | |
| * | |
| * @param array $query Array of query string arguments | |
| * @throws Http\Client\Exception\ExceptionInterface | |
| */ | |
| public function get(string $path, array $query = []) : Response | |
| { | |
| $client = $this->getHttpClient(); | |
| $this->init($path, $client); | |
| $client->setParameterGet($query); | |
| $client->setMethod(Http\Request::METHOD_GET); | |
| $response = $client->send(); | |
| return new Response($response); | |
| } | |
| /** | |
| * Performs an HTTP POST request to $path. | |
| * | |
| * Used internally for all Twitter API POST calls, this method returns a | |
| * Response instance from a request made to API_BASE_URL + $path + '.json'. | |
| * | |
| * $data may be: | |
| * | |
| * - null, in which case no request body is sent. | |
| * - a string, in which case the value is used for the request body. | |
| * - an array or object, in which case the value is passed to json_encode(), | |
| * and the resultant string passed as the request body. | |
| * | |
| * Call this method if you wish to make a POST request to an endpoint that | |
| * does not yet have a corresponding method in this class. As an example: | |
| * | |
| * <code> | |
| * $response = $twitter->post('collections/entries/add', [ | |
| * 'id' => $collectionId, | |
| * 'tweet_id' => $statusId, | |
| * ]); | |
| * if ($response->isError()) { | |
| * echo "Error adding tweet to collection:\n"; | |
| * foreach ($response->response->errors as $error) { | |
| * printf("- %s: %s\n", $error->change->tweet_id, $error->reason); | |
| * } | |
| * } | |
| * </code> | |
| * | |
| * @param string $path | |
| * @param null|string|array|\stdClass $data Raw data to send | |
| * @throws Http\Client\Exception\ExceptionInterface | |
| */ | |
| public function post(string $path, $data = null) : Response | |
| { | |
| $client = $this->getHttpClient(); | |
| $this->init($path, $client); | |
| $response = $this->performPost( | |
| Http\Request::METHOD_POST, | |
| $data, | |
| $client, | |
| in_array($path, self::PATHS_JSON_PAYLOAD, true) | |
| ); | |
| return new Response($response); | |
| } | |
| /** | |
| * Verify account credentials. | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function accountVerifyCredentials() : Response | |
| { | |
| return $this->get('account/verify_credentials'); | |
| } | |
| /** | |
| * Returns the number of api requests you have left per hour. | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function applicationRateLimitStatus() : Response | |
| { | |
| return $this->get('application/rate_limit_status'); | |
| } | |
| /** | |
| * Blocks the user specified in the ID parameter as the authenticating user. | |
| * | |
| * Destroys a friendship to the blocked user if it exists. | |
| * | |
| * @param int|string $id The ID or screen name of a user to block. | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function blocksCreate($id) : Response | |
| { | |
| $path = 'blocks/create'; | |
| $params = $this->createUserParameter($id, []); | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Un-blocks the user specified in the ID parameter for the authenticating user. | |
| * | |
| * @param int|string $id The ID or screen_name of the user to un-block. | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function blocksDestroy($id) : Response | |
| { | |
| $path = 'blocks/destroy'; | |
| $params = $this->createUserParameter($id, []); | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Returns an array of user ids that the authenticating user is blocking. | |
| * | |
| * @param int $cursor Optional. Specifies the cursor position at which to | |
| * begin listing ids; defaults to first "page" of results. | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function blocksIds(int $cursor = -1) : Response | |
| { | |
| $path = 'blocks/ids'; | |
| return $this->get($path, ['cursor' => $cursor]); | |
| } | |
| /** | |
| * Returns an array of user objects that the authenticating user is blocking. | |
| * | |
| * @param int $cursor Optional. Specifies the cursor position at which to | |
| * begin listing ids; defaults to first "page" of results. | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function blocksList(int $cursor = -1) : Response | |
| { | |
| $path = 'blocks/list'; | |
| return $this->get($path, ['cursor' => $cursor]); | |
| } | |
| /** | |
| * Destroy a direct message | |
| * | |
| * @param string|int $id ID of message to destroy. | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function directMessagesDestroy($id) : Response | |
| { | |
| $path = 'direct_messages/destroy'; | |
| $params = ['id' => $this->validInteger($id)]; | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Retrieve direct messages for the current user. | |
| * | |
| * $options may include one or more of the following keys: | |
| * | |
| * - count: return page X of results | |
| * - since_id: return statuses only greater than the one specified | |
| * - max_id: return statuses with an ID less than (older than) or equal to that specified | |
| * - include_entities: setting to false will disable embedded entities | |
| * - skip_status:setting to true, "t", or 1 will omit the status in returned users | |
| * | |
| * @param array $options | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function directMessagesMessages(array $options = []) : Response | |
| { | |
| $path = 'direct_messages'; | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'count': | |
| $params['count'] = (int) $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| case 'skip_status': | |
| $params['skip_status'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Send a direct message to a user. | |
| * | |
| * Proxies to `directMessagesEventsNew()`, as the `direct_messages/new` | |
| * path is deprecated. | |
| * | |
| * @param int|string $user User to whom to send message | |
| * @throws Exception\InvalidArgumentException if message is empty | |
| * @throws Exception\OutOfRangeException if message is too long | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function directMessagesNew($user, string $text, array $extraParams = []) : Response | |
| { | |
| return $this->directMessagesEventsNew($user, $text, $extraParams); | |
| } | |
| /** | |
| * Send a direct message to a user. | |
| * | |
| * If `$extraParams` contains a `media_id` parameter, that value will be | |
| * used to provide a media attachment for the message. | |
| * | |
| * @param int|string $user User to whom to send message | |
| * @throws Exception\InvalidArgumentException if message is empty | |
| * @throws Exception\OutOfRangeException if message is too long | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function directMessagesEventsNew($user, string $text, array $extraParams = []) : Response | |
| { | |
| $path = 'direct_messages/events/new'; | |
| $len = strlen(utf8_decode($text)); | |
| if (0 === $len) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Direct message must contain at least one character' | |
| ); | |
| } | |
| if (10000 < $len) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Direct message must be no more than 10000 characters' | |
| ); | |
| } | |
| if (! $this->validInteger($user)) { | |
| $response = $this->usersShow($user); | |
| if (! $response->isSuccess()) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Invalid user provided; must be a Twitter user ID or screen name' | |
| ); | |
| } | |
| $user = $response->id_str; | |
| } | |
| $params = [ | |
| 'type' => 'message_create', | |
| 'message_create' => [ | |
| 'target' => [ | |
| 'recipient_id' => $user, | |
| ], | |
| 'message_data' => [ | |
| 'text' => $text, | |
| ], | |
| ], | |
| ]; | |
| if (isset($extraParams['media_id'])) { | |
| $params['message_create']['message_data']['attachment'] = [ | |
| 'type' => 'media', | |
| 'media' => [ | |
| 'id' => $extraParams['media_id'], | |
| ], | |
| ]; | |
| } | |
| return $this->post($path, ['event' => $params]); | |
| } | |
| /** | |
| * Retrieve list of direct messages sent by current user. | |
| * | |
| * $options may include one or more of the following keys: | |
| * | |
| * - count: return page X of results | |
| * - page: return starting at page | |
| * - since_id: return statuses only greater than the one specified | |
| * - max_id: return statuses with an ID less than (older than) or equal to that specified | |
| * - include_entities: setting to false will disable embedded entities | |
| * | |
| * @param array $options | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function directMessagesSent(array $options = []) : Response | |
| { | |
| $path = 'direct_messages/sent'; | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'count': | |
| $params['count'] = (int) $value; | |
| break; | |
| case 'page': | |
| $params['page'] = (int) $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Mark a status as a favorite. | |
| * | |
| * @param int|string $id Status ID you want to mark as a favorite | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function favoritesCreate($id) : Response | |
| { | |
| $path = 'favorites/create'; | |
| $params = ['id' => $this->validInteger($id)]; | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Remove a favorite. | |
| * | |
| * @param int|string $id Status ID you want to de-list as a favorite | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function favoritesDestroy($id) : Response | |
| { | |
| $path = 'favorites/destroy'; | |
| $params = ['id' => $this->validInteger($id)]; | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Fetch favorites. | |
| * | |
| * $options may contain one or more of the following: | |
| * | |
| * - user_id: Id of a user for whom to fetch favorites | |
| * - screen_name: Screen name of a user for whom to fetch favorites | |
| * - count: number of tweets to attempt to retrieve, up to 200 | |
| * - since_id: return results only after the specified tweet id | |
| * - max_id: return results with an ID less than (older than) or equal to the specified ID | |
| * - include_entities: when set to false, entities member will be omitted | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function favoritesList(array $options = []) : Response | |
| { | |
| $path = 'favorites/list'; | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'user_id': | |
| $params['user_id'] = $this->validInteger($value); | |
| break; | |
| case 'screen_name': | |
| $params['screen_name'] = $value; | |
| break; | |
| case 'count': | |
| $params['count'] = (int) $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Get a list of up to 5000 follower IDs. | |
| * | |
| * Get a list of up to 5000 follower ids for the logged in account or for the | |
| * screen name you pass in. Returns the next cursor if there are more to be | |
| * returned. | |
| * | |
| * @param int|string $id | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function followersIds($id, array $params = []) : Response | |
| { | |
| $path = 'followers/ids'; | |
| $params = $this->createUserParameter($id, $params); | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Returns a list of IDs of the current logged in user's friends or the | |
| * friends of the screen name passed in as | |
| * part of the parameters array. | |
| * | |
| * Returns the next cursor if there are more to be returned. | |
| * | |
| * @param int|string $id | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function friendsIds($id, array $params = []) : Response | |
| { | |
| $path = 'friends/ids'; | |
| $params = $this->createUserParameter($id, $params); | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Create friendship. | |
| * | |
| * $params are additional parameters to pass to the API, and may include | |
| * any or all of the following: | |
| * | |
| * - user_id | |
| * - screen_name | |
| * - follow | |
| * | |
| * @param int|string $id User ID or name of new friend | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function friendshipsCreate($id, array $params = []) : Response | |
| { | |
| $path = 'friendships/create'; | |
| $params = $this->createUserParameter($id, $params); | |
| $allowed = [ | |
| 'user_id' => null, | |
| 'screen_name' => null, | |
| 'follow' => null, | |
| ]; | |
| $params = array_intersect_key($params, $allowed); | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Get a list of the friends that the logged in user has. | |
| * | |
| * Returns the next cursor if there are more to be returned. | |
| * | |
| * $id may be one of any of the following: | |
| * | |
| * - a single user identifier | |
| * - a single screen name | |
| * - a list of user identifiers | |
| * - a list of screen names | |
| * | |
| * @param int|string|array $id | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function friendshipsLookup($id, array $params = []) : Response | |
| { | |
| $path = 'friendships/lookup'; | |
| $params = $this->createUserListParameter($id, $params, __METHOD__); | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Destroy friendship. | |
| * | |
| * @param int|string $id User ID or name of friend to remove | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function friendshipsDestroy($id) : Response | |
| { | |
| $path = 'friendships/destroy'; | |
| $params = $this->createUserParameter($id, []); | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Get a list of the users belonging to a given list | |
| * | |
| * Returns the next cursor if there are more to be returned. | |
| * | |
| * If the $listIdOrSlug represents a list's slug, then one of the following | |
| * parameters MUST be present: | |
| * | |
| * - `owner_id`, a valid integer identifier of a Twitter user | |
| * - `owner_screen_name`, a valid string identifier of a Twitter user | |
| * | |
| * If either are missing, or malformed, an exception is raised. | |
| * | |
| * @param int|string $listIdOrSlug | |
| * @throws Exception\InvalidArgumentException if $listIdOrSlug is a string | |
| * slug and neither the `owner_id` nor the `owner_screen_name` are | |
| * provided in `$params`. | |
| * @throws Exception\InvalidArgumentException if $listIdOrSlug is a string | |
| * slug and the `owner_id` provided is not a valid integer. | |
| * @throws Exception\InvalidArgumentException if $listIdOrSlug is a string | |
| * slug and the `owner_screen_name` provided is not valid. | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function listsMembers($listIdOrSlug, array $params = []) : Response | |
| { | |
| $path = 'lists/members'; | |
| if (0 !== $this->validInteger($listIdOrSlug)) { | |
| $params['list_id'] = $listIdOrSlug; | |
| return $this->get($path, $params); | |
| } | |
| $params['slug'] = $listIdOrSlug; | |
| if (! array_key_exists('owner_id', $params) && ! array_key_exists('owner_screen_name', $params)) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| '%s was provided a list slug, but is missing owner info; ' | |
| . 'please provide one of either the "owner_id" or ' | |
| . '"owner_screen_name" parameters when calling the method', | |
| __METHOD__ | |
| )); | |
| } | |
| if (array_key_exists('owner_id', $params)) { | |
| if (0 === $this->validInteger($params['owner_id'])) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| '%s was provided a list slug, but an invalid owner_id parameter; ' | |
| . 'must be an integer', | |
| __METHOD__ | |
| )); | |
| } | |
| return $this->get($path, $params); | |
| } | |
| if (! is_string($params['owner_screen_name'])) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| '%s was provided a list slug, but an invalid owner_screen_name parameter; ' | |
| . 'must be an integer', | |
| __METHOD__ | |
| )); | |
| } | |
| try { | |
| $this->validateScreenName($params['owner_screen_name']); | |
| } catch (Exception\InvalidArgumentException $e) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| '%s was provided a list slug, but an invalid owner_screen_name parameter; ' | |
| . 'must be an integer', | |
| __METHOD__ | |
| ), 0, $e); | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Get a list of the lists that the logged in user is a member of. | |
| * | |
| * Returns the next cursor if there are more to be returned. | |
| * | |
| * @param int|string $id | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function listsMemberships($id, array $params = []) : Response | |
| { | |
| $path = 'lists/memberships'; | |
| $params = $this->createUserParameter($id, $params); | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Returns the subscribers of the specified list. Private list subscribers | |
| * will only be shown if the authenticated user owns the specified list. | |
| * | |
| * Returns the next cursor if there are more to be returned. | |
| * | |
| * @param int|string $id | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function listsSubscribers($id, array $params = []) : Response | |
| { | |
| $path = 'lists/subscribers'; | |
| $params = $this->createUserParameter($id, $params); | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Search tweets. | |
| * | |
| * $options may include any of the following: | |
| * | |
| * - geocode: a string of the form "latitude, longitude, radius" | |
| * - lang: restrict tweets to the two-letter language code | |
| * - locale: query is in the given two-letter language code | |
| * - result_type: what type of results to receive: mixed, recent, or popular | |
| * - count: number of tweets to return per page; up to 100 | |
| * - until: return tweets generated before the given date | |
| * - since_id: return resutls with an ID greater than (more recent than) the given ID | |
| * - max_id: return results with an ID less than (older than) the given ID | |
| * - include_entities: whether or not to include embedded entities | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function searchTweets(string $query, array $options = []) : Response | |
| { | |
| $path = 'search/tweets'; | |
| $len = strlen(utf8_decode($query)); | |
| if (0 == $len) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Query must contain at least one character' | |
| ); | |
| } | |
| $params = ['q' => $query]; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'geocode': | |
| if (substr_count($value, ',') !== 2) { | |
| throw new Exception\InvalidArgumentException( | |
| '"geocode" must be of the format "latitude,longitude,radius"' | |
| ); | |
| } | |
| list($latitude, $longitude, $radius) = explode(',', $value, 3); | |
| $radius = trim($radius); | |
| if (! preg_match('/^\d+(mi|km)$/', $radius)) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Radius segment of "geocode" must be of the format "[unit](mi|km)"' | |
| ); | |
| } | |
| $latitude = (float) $latitude; | |
| $longitude = (float) $longitude; | |
| $params['geocode'] = $latitude . ',' . $longitude . ',' . $radius; | |
| break; | |
| case 'lang': | |
| if (strlen($value) > 2) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Query language must be a 2 character string' | |
| ); | |
| } | |
| $params['lang'] = strtolower($value); | |
| break; | |
| case 'locale': | |
| if (strlen($value) > 2) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Query locale must be a 2 character string' | |
| ); | |
| } | |
| $params['locale'] = strtolower($value); | |
| break; | |
| case 'result_type': | |
| $value = strtolower($value); | |
| if (! in_array($value, ['mixed', 'recent', 'popular'])) { | |
| throw new Exception\InvalidArgumentException( | |
| 'result_type must be one of "mixed", "recent", or "popular"' | |
| ); | |
| } | |
| $params['result_type'] = $value; | |
| break; | |
| case 'count': | |
| $value = (int) $value; | |
| if (1 > $value || 100 < $value) { | |
| throw new Exception\InvalidArgumentException( | |
| 'count must be between 1 and 100' | |
| ); | |
| } | |
| $params['count'] = $value; | |
| break; | |
| case 'until': | |
| if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) { | |
| throw new Exception\InvalidArgumentException( | |
| '"until" must be a date in the format YYYY-MM-DD' | |
| ); | |
| } | |
| $params['until'] = $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Destroy a status message. | |
| * | |
| * @param int|string $id ID of status to destroy | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesDestroy($id) : Response | |
| { | |
| $path = 'statuses/destroy/' . $this->validInteger($id); | |
| return $this->post($path); | |
| } | |
| /** | |
| * Home Timeline Status. | |
| * | |
| * $options may include one or more of the following keys: | |
| * | |
| * - count: number of tweets to attempt to retrieve, up to 200 | |
| * - since_id: return results only after the specified tweet id | |
| * - max_id: return results with an ID less than (older than) or equal to the specified ID | |
| * - trim_user: when set to true, "t", or 1, user object in tweets will include only author's ID. | |
| * - contributor_details: when set to true, includes screen_name of each contributor | |
| * - include_entities: when set to false, entities member will be omitted | |
| * - exclude_replies: when set to true, will strip replies appearing in the timeline | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesHomeTimeline(array $options = []) : Response | |
| { | |
| $path = 'statuses/home_timeline'; | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'count': | |
| $params['count'] = (int) $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'tweet_mode': | |
| unset($params[$key]); | |
| $params['tweet_mode'] = 'extended'; | |
| break; | |
| case 'trim_user': | |
| if (in_array($value, [true, 'true', 't', 1, '1'])) { | |
| $value = true; | |
| } else { | |
| $value = false; | |
| } | |
| $params['trim_user'] = $value; | |
| break; | |
| case 'contributor_details': | |
| $params['contributor_details'] = (bool) $value; | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| case 'exclude_replies': | |
| $params['exclude_replies'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Get metions. | |
| * | |
| * $options may include one or more of the following keys: | |
| * | |
| * - count: number of tweets to attempt to retrieve, up to 200 | |
| * - since_id: return results only after the specified tweet id | |
| * - max_id: return results with an ID less than (older than) or equal to the specified ID | |
| * - trim_user: when set to true, "t", or 1, user object in tweets will include only author's ID. | |
| * - contributor_details: when set to true, includes screen_name of each contributor | |
| * - include_entities: when set to false, entities member will be omitted | |
| * | |
| * @param array $options | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesMentionsTimeline(array $options = []) : Response | |
| { | |
| $path = 'statuses/mentions_timeline'; | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'count': | |
| $params['count'] = (int) $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'tweet_mode': | |
| unset($params[$key]); | |
| $params['tweet_mode'] = 'extended'; | |
| break; | |
| case 'trim_user': | |
| if (in_array($value, [true, 'true', 't', 1, '1'])) { | |
| $value = true; | |
| } else { | |
| $value = false; | |
| } | |
| $params['trim_user'] = $value; | |
| break; | |
| case 'contributor_details:': | |
| $params['contributor_details:'] = (bool) $value; | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Random sampling of public statuses. | |
| * | |
| * Note: this method is not in the current Twitter API documentation, and | |
| * may not work currently. | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesSample() : Response | |
| { | |
| $path = 'statuses/sample'; | |
| return $this->get($path); | |
| } | |
| /** | |
| * Show a single status. | |
| * | |
| * $options is an array of additional data to pass to the API in order to | |
| * detail how to return results; it may include any or all of the following | |
| * options: | |
| * | |
| * - tweet_mode: currently, only allows "extended", to provide maximum tweet detail | |
| * - include_entities: whether or not to return media entities | |
| * - trim_user: whether or not to return user data with each tweet | |
| * - include_my_retweet: whether or not to return statuses that are your own retweets | |
| * | |
| * @param int|string $id Id of status to show | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesShow($id, array $options = []) : Response | |
| { | |
| $path = 'statuses/show/' . $this->validInteger($id); | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'tweet_mode': | |
| unset($params[$key]); | |
| $params['tweet_mode'] = 'extended'; | |
| break; | |
| case 'include_entities': | |
| $params[strtolower($key)] = (bool) $value; | |
| break; | |
| case 'trim_user': | |
| $params[strtolower($key)] = (bool) $value; | |
| break; | |
| case 'include_my_retweet': | |
| $params[strtolower($key)] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Update user's current status. | |
| * | |
| * $extraAttributes currently supports the following options: | |
| * | |
| * - media_ids: an array of media identifiers as returned by uploading | |
| * media via the Twitter API, and which should be attached to the tweet. | |
| * | |
| * @param string $status | |
| * @param null|int|string $inReplyToStatusId | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\OutOfRangeException if message is too long | |
| * @throws Exception\InvalidArgumentException if message is empty | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesUpdate(string $status, $inReplyToStatusId = null, $extraAttributes = []) : Response | |
| { | |
| $path = 'statuses/update'; | |
| $len = strlen(utf8_decode($status)); | |
| if ($len > self::STATUS_MAX_CHARACTERS) { | |
| throw new Exception\OutOfRangeException(sprintf( | |
| 'Status must be no more than %d characters in length; received %d', | |
| self::STATUS_MAX_CHARACTERS, | |
| $len | |
| )); | |
| } elseif (0 == $len) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Status must contain at least one character' | |
| ); | |
| } | |
| $params = ['status' => $status]; | |
| if (isset($extraAttributes['media_ids']) | |
| && is_array($extraAttributes['media_ids']) | |
| && ! empty($extraAttributes['media_ids']) | |
| ) { | |
| $params['media_ids'] = implode(',', $extraAttributes['media_ids']); | |
| } | |
| $inReplyToStatusId = $this->validInteger($inReplyToStatusId); | |
| if ($inReplyToStatusId) { | |
| $params['in_reply_to_status_id'] = $inReplyToStatusId; | |
| } | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * User Timeline. | |
| * | |
| * $options may include one or more of the following keys | |
| * | |
| * - user_id: Id of a user for whom to fetch favorites | |
| * - screen_name: Screen name of a user for whom to fetch favorites | |
| * - count: number of tweets to attempt to retrieve, up to 200 | |
| * - since_id: return results only after the specified tweet id | |
| * - max_id: return results with an ID less than (older than) or equal to the specified ID | |
| * - trim_user: when set to true, "t", or 1, user object in tweets will include only author's ID. | |
| * - exclude_replies: when set to true, will strip replies appearing in the timeline | |
| * - contributor_details: when set to true, includes screen_name of each contributor | |
| * - include_rts: when set to false, will strip native retweets | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function statusesUserTimeline(array $options = []) : Response | |
| { | |
| $path = 'statuses/user_timeline'; | |
| $params = []; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'user_id': | |
| $params['user_id'] = $this->validInteger($value); | |
| break; | |
| case 'screen_name': | |
| $params['screen_name'] = $this->validateScreenName($value); | |
| break; | |
| case 'count': | |
| $params['count'] = (int) $value; | |
| break; | |
| case 'since_id': | |
| $params['since_id'] = $this->validInteger($value); | |
| break; | |
| case 'max_id': | |
| $params['max_id'] = $this->validInteger($value); | |
| break; | |
| case 'tweet_mode': | |
| unset($params[$key]); | |
| $params['tweet_mode'] = 'extended'; | |
| break; | |
| case 'trim_user': | |
| if (in_array($value, [true, 'true', 't', 1, '1'])) { | |
| $value = true; | |
| } else { | |
| $value = false; | |
| } | |
| $params['trim_user'] = $value; | |
| break; | |
| case 'contributor_details:': | |
| $params['contributor_details:'] = (bool) $value; | |
| break; | |
| case 'exclude_replies': | |
| $params['exclude_replies'] = (bool) $value; | |
| break; | |
| case 'include_rts': | |
| $params['include_rts'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Pass in one or more twitter IDs and it will return a list of user objects. | |
| * | |
| * This is the most effecient way of gathering bulk user data. | |
| * | |
| * $id may be one of any of the following: | |
| * | |
| * - a single user identifier | |
| * - a single screen name | |
| * - a list of user identifiers | |
| * - a list of screen names | |
| * | |
| * @param int|string|array $id | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function usersLookup($id, array $params = []) : Response | |
| { | |
| $path = 'users/lookup'; | |
| $params = $this->createUserListParameter($id, $params, __METHOD__); | |
| return $this->post($path, $params); | |
| } | |
| /** | |
| * Search users. | |
| * | |
| * $options may include any of the following: | |
| * | |
| * - page: the page of results to retrieve | |
| * - count: the number of users to retrieve per page; max is 20 | |
| * - include_entities: if set to boolean true, include embedded entities | |
| * | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function usersSearch(string $query, array $options = []) : Response | |
| { | |
| $path = 'users/search'; | |
| $len = strlen(utf8_decode($query)); | |
| if (0 == $len) { | |
| throw new Exception\InvalidArgumentException( | |
| 'Query must contain at least one character' | |
| ); | |
| } | |
| $params = ['q' => $query]; | |
| foreach ($options as $key => $value) { | |
| switch (strtolower($key)) { | |
| case 'count': | |
| $value = (int) $value; | |
| if (1 > $value || 20 < $value) { | |
| throw new Exception\InvalidArgumentException( | |
| 'count must be between 1 and 20' | |
| ); | |
| } | |
| $params['count'] = $value; | |
| break; | |
| case 'page': | |
| $params['page'] = (int) $value; | |
| break; | |
| case 'include_entities': | |
| $params['include_entities'] = (bool) $value; | |
| break; | |
| default: | |
| break; | |
| } | |
| } | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Show extended information on a user. | |
| * | |
| * @param int|string $id User ID or name | |
| * @throws Http\Client\Exception\ExceptionInterface if HTTP request fails or times out | |
| * @throws Exception\DomainException if unable to decode JSON payload | |
| */ | |
| public function usersShow($id) : Response | |
| { | |
| $path = 'users/show'; | |
| $params = $this->createUserParameter($id, []); | |
| return $this->get($path, $params); | |
| } | |
| /** | |
| * Initialize HTTP authentication | |
| * | |
| * @throws Exception\DomainException if unauthorised | |
| */ | |
| public function init(string $path, Http\Client $client) : void | |
| { | |
| if (! $this->isAuthorised($client) && $this->getUsername() !== null) { | |
| throw new Exception\DomainException( | |
| 'Twitter session is unauthorised. You need to initialize ' | |
| . __CLASS__ . ' with an OAuth Access Token or use ' | |
| . 'its OAuth functionality to obtain an Access Token before ' | |
| . 'attempting any API actions that require authorisation' | |
| ); | |
| } | |
| // Reset on every request to prevent parameters leaking between them. | |
| $client->resetParameters(); | |
| $client->setUri(static::API_BASE_URI . $path . '.json'); | |
| if (null === $this->cookieJar) { | |
| $client->clearCookies(); | |
| $this->cookieJar = $client->getCookies(); | |
| return; | |
| } | |
| $client->setCookies($this->cookieJar); | |
| } | |
| /** | |
| * Protected function to validate a status or user identifier. | |
| * | |
| * If the value consists of solely digits, it is valid, and will be | |
| * returned verbatim. Otherwise, a zero is returned. | |
| * | |
| * @param string|int $int | |
| */ | |
| protected function validInteger($int) : int | |
| { | |
| if (is_int($int) && $int > -1) { | |
| return $int; | |
| } | |
| if (is_string($int) && preg_match('/^(\d+)$/', $int)) { | |
| return $int; | |
| } | |
| return 0; | |
| } | |
| /** | |
| * Validate a screen name using Twitter rules. | |
| * | |
| * @throws Exception\InvalidArgumentException | |
| */ | |
| protected function validateScreenName(string $name) : string | |
| { | |
| if (! is_string($name) || ! preg_match('/^[a-zA-Z0-9_]{1,15}$/', $name)) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| 'Screen name, "%s" should only contain alphanumeric characters and' | |
| . ' underscores, and not exceed 15 characters.', | |
| $name | |
| )); | |
| } | |
| return $name; | |
| } | |
| /** | |
| * Perform a POST or PUT | |
| * | |
| * Performs a POST or PUT request. Any data provided is set in the HTTP | |
| * client. String data is pushed in as raw POST data; array or object data | |
| * is JSON-encoded before being passed to the request body. | |
| * | |
| * @param null|string|array|\stdClass $data Raw data to send | |
| * @param bool $asJson Whether or not the data should be submitted as JSON | |
| * (vs form urlencoded, which is the default) | |
| */ | |
| protected function performPost(string $method, $data, Http\Client $client, bool $asJson) : Http\Response | |
| { | |
| $client->setMethod($method); | |
| $asJson | |
| ? $this->prepareJsonPayloadForClient($client, $data) | |
| : $this->prepareFormPayloadForClient($client, $data); | |
| return $client->send(); | |
| } | |
| /** | |
| * Create a parameter representing the user. | |
| * | |
| * Determines if $id is an integer, and, if so, sets the "user_id" parameter. | |
| * If not, assumes the $id is the "screen_name". | |
| * | |
| * Returns the $params with the appropriate key added. | |
| * | |
| * @param int|string $id | |
| * @param array $params | |
| * @throws Exception\InvalidArgumentException if the value is neither an integer nor a string | |
| */ | |
| protected function createUserParameter($id, array $params) : array | |
| { | |
| if (! is_string($id) && ! is_int($id)) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| '$id must be an integer or a string, received %s', | |
| is_object($id) ? get_class($id) : gettype($id) | |
| )); | |
| } | |
| if (0 !== $this->validInteger($id)) { | |
| $params['user_id'] = $id; | |
| return $params; | |
| } | |
| if (! is_string($id)) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| '$id must be an integer or a string, received %s', | |
| gettype($id) | |
| )); | |
| } | |
| $params['screen_name'] = $this->validateScreenName($id); | |
| return $params; | |
| } | |
| /** | |
| * Prepares a list of identifiers for use with endpoints accepting lists of users. | |
| * | |
| * The various `lookup` endpoints allow passing either: | |
| * | |
| * - a single user identifier | |
| * - a single screen name | |
| * - a list of user identifiers | |
| * - a list of screen names | |
| * | |
| * This method checks for each of these conditions. For scalar $ids, it | |
| * proxies to {@link createUserParameter}. Otherwise, it checks to ensure | |
| * that all values are of the same type. For identifiers, it then | |
| * concatenates the values and returns them in the `user_id` parameter. | |
| * For screen names, it validates them first, before concatenating and | |
| * returning them via the `screen_name` parameter. | |
| * | |
| * @param int|string|array $ids | |
| * @throws Exception\InvalidArgumentException for a non-int/string/array $ids value. | |
| * @throws Exception\InvalidArgumentException if an array of $ids exceeds 100 items. | |
| * @throws Exception\InvalidArgumentException if an array of $ids are not all of the same type. | |
| * @throws Exception\InvalidArgumentException if any screen name element is invalid. | |
| */ | |
| protected function createUserListParameter($ids, array $params, string $context) : array | |
| { | |
| if (! is_array($ids)) { | |
| return $this->createUserParameter($ids, $params); | |
| } | |
| if (100 < count($ids)) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| 'Lists of identifier(s) or screen name(s) provided for %s; ' | |
| . 'must contain no more than 100 items. ' | |
| . 'Received %d', | |
| $context, | |
| count($ids) | |
| )); | |
| } | |
| $detectedType = array_reduce($ids, function ($detectedType, $id) { | |
| if (false === $detectedType) { | |
| return $detectedType; | |
| } | |
| $idType = 0 !== $this->validInteger($id) | |
| ? 'user_id' | |
| : 'screen_name'; | |
| if (null === $detectedType) { | |
| return $idType; | |
| } | |
| return $detectedType === $idType | |
| ? $detectedType | |
| : false; | |
| }, null); | |
| if (false === $detectedType) { | |
| throw new Exception\InvalidArgumentException(sprintf( | |
| 'Invalid identifier(s) or screen name(s) provided for %s; ' | |
| . 'all values must either be identifiers OR screen names. ' | |
| . 'You cannot provide items of both types.', | |
| $context | |
| )); | |
| } | |
| if ($detectedType === 'user_id') { | |
| $params['user_id'] = implode(',', $ids); | |
| return $params; | |
| } | |
| array_walk($ids, Closure::fromCallable([$this, 'validateScreenName'])); | |
| $params['screen_name'] = implode(',', $ids); | |
| return $params; | |
| } | |
| /** | |
| * Prepare a JSON payload for the HTTP client. | |
| */ | |
| private function prepareJsonPayloadForClient(Http\Client $client, $data) | |
| { | |
| if (is_array($data) || is_object($data)) { | |
| $data = json_encode($data, $this->jsonFlags); | |
| } | |
| if (empty($data) || ! is_string($data)) { | |
| return; | |
| } | |
| $client->getRequest() | |
| ->getHeaders() | |
| ->addHeaderLine('Content-Type', 'application/json'); | |
| $client->setRawBody($data); | |
| } | |
| /** | |
| * Prepare a form-url-encoded payload for the HTTP client. | |
| */ | |
| private function prepareFormPayloadForClient(Http\Client $client, $data) | |
| { | |
| if (! is_array($data)) { | |
| return; | |
| } | |
| $client->setParameterPost($data); | |
| } | |
| } |