From daf2fc1d4d893d8e601cb6c2b6516208ec1473e5 Mon Sep 17 00:00:00 2001 From: David Grudl Date: Thu, 22 Apr 2021 19:10:49 +0200 Subject: [PATCH 1/8] coding style --- examples/custom-request.php | 4 ++-- examples/load.php | 4 ++-- examples/search.php | 4 ++-- src/OAuth.php | 33 ++++++++++++++++++++++++--------- src/Twitter.php | 35 ++++++++++++++++++++++++----------- 5 files changed, 54 insertions(+), 26 deletions(-) diff --git a/examples/custom-request.php b/examples/custom-request.php index d4c7778..36d77e5 100644 --- a/examples/custom-request.php +++ b/examples/custom-request.php @@ -16,11 +16,11 @@ Twitter retweets of me diff --git a/examples/load.php b/examples/load.php index a223e3a..6a1fa39 100644 --- a/examples/load.php +++ b/examples/load.php @@ -19,11 +19,11 @@ Twitter timeline demo diff --git a/examples/search.php b/examples/search.php index 6488efb..49344ad 100644 --- a/examples/search.php +++ b/examples/search.php @@ -16,11 +16,11 @@ Twitter search demo diff --git a/src/OAuth.php b/src/OAuth.php index 38d3505..0bcb4ca 100644 --- a/src/OAuth.php +++ b/src/OAuth.php @@ -302,12 +302,17 @@ public function __construct(string $http_method, string $http_url, array $parame /** * attempt to build up a request from what was passed to the server */ - public static function from_request(string $http_method = null, string $http_url = null, array $parameters = null): self - { + public static function from_request( + string $http_method = null, + string $http_url = null, + array $parameters = null + ): self { $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != 'on') ? 'http' : 'https'; - $http_url = ($http_url) ? $http_url : $scheme . + $http_url = ($http_url) + ? $http_url + : $scheme . '://' . $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT'] . @@ -339,7 +344,10 @@ public static function from_request(string $http_method = null, string $http_url // We have a Authorization-header with OAuth data. Parse the header // and add those overriding any duplicates from GET or POST - if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') { + if ( + isset($request_headers['Authorization']) + && substr($request_headers['Authorization'], 0, 6) == 'OAuth ' + ) { $header_parameters = Util::split_header( $request_headers['Authorization'] ); @@ -354,8 +362,13 @@ public static function from_request(string $http_method = null, string $http_url /** * pretty much a helper function to set up the request */ - public static function from_consumer_and_token(Consumer $consumer, ?Token $token, string $http_method, string $http_url, array $parameters = null): self - { + public static function from_consumer_and_token( + Consumer $consumer, + ?Token $token, + string $http_method, + string $http_url, + array $parameters = null + ): self { $parameters = $parameters ?: []; $defaults = [ 'oauth_version' => self::$version, @@ -392,7 +405,7 @@ public function set_parameter(string $name, $value, bool $allow_duplicates = tru public function get_parameter(string $name) { - return isset($this->parameters[$name]) ? $this->parameters[$name] : null; + return $this->parameters[$name] ?? null; } @@ -465,7 +478,9 @@ public function get_normalized_http_url(): string $parts = parse_url($this->http_url); $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http'; - $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80'); + $port = (isset($parts['port'])) + ? $parts['port'] + : (($scheme == 'https') ? '443' : '80'); $host = (isset($parts['host'])) ? $parts['host'] : ''; $path = (isset($parts['path'])) ? $parts['path'] : ''; @@ -581,7 +596,7 @@ class Util public static function urlencode_rfc3986($input) { if (is_array($input)) { - return array_map([__CLASS__, 'urlencode_rfc3986'], $input); + return array_map([self::class, 'urlencode_rfc3986'], $input); } elseif (is_scalar($input)) { return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode((string) $input))); } else { diff --git a/src/Twitter.php b/src/Twitter.php index eec96b5..9600eb3 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -54,8 +54,12 @@ class Twitter * Creates object using consumer and access keys. * @throws Exception when CURL extension is not loaded */ - public function __construct(string $consumerKey, string $consumerSecret, string $accessToken = null, string $accessTokenSecret = null) - { + public function __construct( + string $consumerKey, + string $consumerSecret, + string $accessToken = null, + string $accessTokenSecret = null + ) { if (!extension_loaded('curl')) { throw new Exception('PHP extension CURL is not loaded.'); } @@ -196,8 +200,12 @@ public function loadUserInfoById(string $id): stdClass * https://dev.twitter.com/rest/reference/get/followers/ids * @throws Exception */ - public function loadUserFollowers(string $username, int $count = 5000, int $cursor = -1, $cacheExpiry = null): stdClass - { + public function loadUserFollowers( + string $username, + int $count = 5000, + int $cursor = -1, + $cacheExpiry = null + ): stdClass { return $this->cachedRequest('followers/ids', [ 'screen_name' => $username, 'count' => $count, @@ -211,8 +219,12 @@ public function loadUserFollowers(string $username, int $count = 5000, int $curs * https://dev.twitter.com/rest/reference/get/followers/list * @throws Exception */ - public function loadUserFollowersList(string $username, int $count = 200, int $cursor = -1, $cacheExpiry = null): stdClass - { + public function loadUserFollowersList( + string $username, + int $count = 200, + int $cursor = -1, + $cacheExpiry = null + ): stdClass { return $this->cachedRequest('followers/list', [ 'screen_name' => $username, 'count' => $count, @@ -347,9 +359,8 @@ public function request(string $resource, string $method, array $data = [], arra $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); if ($code >= 400) { - throw new Exception(isset($payload->errors[0]->message) - ? $payload->errors[0]->message - : "Server error #$code with answer $result", + throw new Exception( + $payload->errors[0]->message ?? "Server error #$code with answer $result", $code ); } elseif ($code === 204) { @@ -379,7 +390,9 @@ public function cachedRequest(string $resource, array $data = [], $cacheExpire = . '.json'; $cache = @json_decode((string) @file_get_contents($cacheFile)); // intentionally @ - $expiration = is_string($cacheExpire) ? strtotime($cacheExpire) - time() : $cacheExpire; + $expiration = is_string($cacheExpire) + ? strtotime($cacheExpire) - time() + : $cacheExpire; if ($cache && @filemtime($cacheFile) + $expiration > time()) { // intentionally @ return $cache; } @@ -424,7 +437,7 @@ public static function clickable(stdClass $status): string } krsort($all); - $s = isset($status->full_text) ? $status->full_text : $status->text; + $s = $status->full_text ?? $status->text; foreach ($all as $pos => $item) { $s = iconv_substr($s, 0, $pos, 'UTF-8') . '' . htmlspecialchars($item[1]) . '' From 6da0a330d9643e45860e5e4d19b6f5ed9f7673d2 Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Mon, 15 Nov 2021 08:50:23 -0800 Subject: [PATCH 2/8] Updating PHPDoc, Adding method to retrieve oauth tokens, updating request method to decode text/html + urlencoded responses from Twitter. --- src/Twitter.php | 295 ++++++++++++++++++++++++++++++------------------ 1 file changed, 188 insertions(+), 107 deletions(-) diff --git a/src/Twitter.php b/src/Twitter.php index 9600eb3..157f42d 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -14,10 +14,8 @@ */ namespace DG\Twitter; - use stdClass; - /** * Twitter API. */ @@ -49,7 +47,6 @@ class Twitter /** @var OAuth\Token */ private $token; - /** * Creates object using consumer and access keys. * @throws Exception when CURL extension is not loaded @@ -70,11 +67,12 @@ public function __construct( } } - - /** - * Tests if user credentials are valid. - * @throws Exception - */ + /** + * Tests if user credentials are valid. + * @return bool + * @throws Exception + * @throws OAuth\Exception + */ public function authenticate(): bool { try { @@ -89,13 +87,16 @@ public function authenticate(): bool } } - - /** - * Sends message to the Twitter. - * https://dev.twitter.com/rest/reference/post/statuses/update - * @param string|array $mediaPath path to local media file to be uploaded - * @throws Exception - */ + /** + * Sends message to the Twitter. + * https://dev.twitter.com/rest/reference/post/statuses/update + * @param string $message + * @param null $mediaPath path to local media file to be uploaded + * @param array $options + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function send(string $message, $mediaPath = null, array $options = []): stdClass { $mediaIds = []; @@ -115,12 +116,15 @@ public function send(string $message, $mediaPath = null, array $options = []): s ); } - - /** - * Sends a direct message to the specified user. - * https://dev.twitter.com/rest/reference/post/direct_messages/new - * @throws Exception - */ + /** + * Sends a direct message to the specified user. + * https://dev.twitter.com/rest/reference/post/direct_messages/new + * @param string $username + * @param string $message + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function sendDirectMessage(string $username, string $message): stdClass { return $this->request( @@ -136,25 +140,29 @@ public function sendDirectMessage(string $username, string $message): stdClass ); } - - /** - * Follows a user on Twitter. - * https://dev.twitter.com/rest/reference/post/friendships/create - * @throws Exception - */ + /** + * Follows a user on Twitter. + * https://dev.twitter.com/rest/reference/post/friendships/create + * @param string $username + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function follow(string $username): stdClass { return $this->request('friendships/create', 'POST', ['screen_name' => $username]); } - - /** - * Returns the most recent statuses. - * https://dev.twitter.com/rest/reference/get/statuses/user_timeline - * @param int $flags timeline (ME | ME_AND_FRIENDS | REPLIES) and optional (RETWEETS) - * @return stdClass[] - * @throws Exception - */ + /** + * Returns the most recent statuses. + * https://dev.twitter.com/rest/reference/get/statuses/user_timeline + * @param int $flags timeline (ME | ME_AND_FRIENDS | REPLIES) and optional (RETWEETS) + * @param int $count + * @param array|null $data + * @return stdClass[] + * @throws Exception + * @throws OAuth\Exception + */ public function load(int $flags = self::ME, int $count = 20, array $data = null): array { static $timelines = [ @@ -172,34 +180,43 @@ public function load(int $flags = self::ME, int $count = 20, array $data = null) ]); } - - /** - * Returns information of a given user. - * https://dev.twitter.com/rest/reference/get/users/show - * @throws Exception - */ + /** + * Returns information of a given user. + * https://dev.twitter.com/rest/reference/get/users/show + * @param string $username + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function loadUserInfo(string $username): stdClass { return $this->cachedRequest('users/show', ['screen_name' => $username]); } - - /** - * Returns information of a given user by id. - * https://dev.twitter.com/rest/reference/get/users/show - * @throws Exception - */ + /** + * Returns information of a given user by id. + * https://dev.twitter.com/rest/reference/get/users/show + * @param string $id + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function loadUserInfoById(string $id): stdClass { return $this->cachedRequest('users/show', ['user_id' => $id]); } - - /** - * Returns IDs of followers of a given user. - * https://dev.twitter.com/rest/reference/get/followers/ids - * @throws Exception - */ + /** + * Returns IDs of followers of a given user. + * https://dev.twitter.com/rest/reference/get/followers/ids + * @param string $username + * @param int $count + * @param int $cursor + * @param null $cacheExpiry + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function loadUserFollowers( string $username, int $count = 5000, @@ -213,12 +230,17 @@ public function loadUserFollowers( ], $cacheExpiry); } - - /** - * Returns list of followers of a given user. - * https://dev.twitter.com/rest/reference/get/followers/list - * @throws Exception - */ + /** + * Returns list of followers of a given user. + * https://dev.twitter.com/rest/reference/get/followers/list + * @param string $username + * @param int $count + * @param int $cursor + * @param null $cacheExpiry + * @return stdClass + * @throws Exception + * @throws OAuth\Exception + */ public function loadUserFollowersList( string $username, int $count = 200, @@ -232,61 +254,69 @@ public function loadUserFollowersList( ], $cacheExpiry); } - - /** - * Destroys status. - * @param int|string $id status to be destroyed - * @throws Exception - */ + /** + * Destroys status. + * @param int|string $id status to be destroyed + * @return false + * @throws Exception + * @throws OAuth\Exception + */ public function destroy($id) { $res = $this->request("statuses/destroy/$id", 'POST', ['id' => $id]); return $res->id ?: false; } - - /** - * Retrieves a single status. - * @param int|string $id status to be retrieved - * @throws Exception - */ + /** + * Retrieves a single status. + * @param int|string $id status to be retrieved + * @return array|bool|mixed + * @throws Exception + * @throws OAuth\Exception + */ public function get($id) { $res = $this->request("statuses/show/$id", 'GET'); return $res; } - - /** - * Returns tweets that match a specified query. - * https://dev.twitter.com/rest/reference/get/search/tweets - * @param string|array - * @throws Exception - * @return stdClass|stdClass[] - */ + /** + * Returns tweets that match a specified query. + * https://dev.twitter.com/rest/reference/get/search/tweets + * @param string|array + * @param bool $full + * @return stdClass|stdClass[] + * @throws Exception + * @throws OAuth\Exception + */ public function search($query, bool $full = false) { $res = $this->request('search/tweets', 'GET', is_array($query) ? $query : ['q' => $query]); return $full ? $res : $res->statuses; } - - /** - * Retrieves the top 50 trending topics for a specific WOEID. - * @param int|string $WOEID Where On Earth IDentifier - */ + /** + * Retrieves the top 50 trending topics for a specific WOEID. + * @param int|string $WOEID Where On Earth IDentifier + * @return array + * @throws Exception + * @throws OAuth\Exception + */ public function getTrends(int $WOEID): array { return $this->request("trends/place.json?id=$WOEID", 'GET'); } - - /** - * Process HTTP request. - * @param string $method GET|POST|JSONPOST|DELETE - * @return mixed - * @throws Exception - */ + /** + * Process HTTP request. + * @param string $resource + * @param string $method GET|POST|JSONPOST|DELETE + * @param array $data + * @param array $files + * @return mixed + * @throws Exception + * @throws OAuth\Exception + */ public function request(string $resource, string $method, array $data = [], array $files = []) { if (!strpos($resource, '://')) { @@ -345,19 +375,31 @@ public function request(string $resource, string $method, array $data = [], arra $curl = curl_init(); curl_setopt_array($curl, $options); + /** + * The result of the request (Raw Body) + */ $result = curl_exec($curl); + /** + * Get the Content-Type from the Response; + */ + $contentType = curl_getinfo($curl, CURLINFO_CONTENT_TYPE); + /** + * Get the Response Code from the Request. + */ + $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); if (curl_errno($curl)) { throw new Exception('Server error: ' . curl_error($curl)); } - - if (strpos(curl_getinfo($curl, CURLINFO_CONTENT_TYPE), 'application/json') !== false) { + /** + * If JSON was returned, decode and return. + */ + if (strpos($contentType, 'application/json') !== false) { $payload = @json_decode($result, false, 128, JSON_BIGINT_AS_STRING); // intentionally @ if ($payload === false) { throw new Exception('Invalid server response'); } } - $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); if ($code >= 400) { throw new Exception( $payload->errors[0]->message ?? "Server error #$code with answer $result", @@ -366,15 +408,59 @@ public function request(string $resource, string $method, array $data = [], arra } elseif ($code === 204) { $payload = true; } - - return $payload; + /** + * If the payload isn't null or undefined. + */ + if (isset($payload)) { + return $payload; + } + /** + * There are instances where the Twitter API returns text/html or urlencoded responses. + * @link https://developer.twitter.com/en/docs/authentication/api-reference/request_token + * Convert the encoded URL to an Array and return it. + */ + else if ($code === 200 && + (strpos($contentType, 'application/x-www-form-urlencoded') !== false) || + (strpos($contentType, 'text/html') !== false)) { + $res = array(); + /** + * Process the url-encoded data and convert it into an array. + */ + foreach (explode("&", $result) as $x) { + $y = explode("=", $x); + $res[$y[0]] = $y[1]; + } + return $res; + } + else { + throw new Exception('Invalid server response (Not Valid)'); + } } - - /** - * Cached HTTP request. - * @return stdClass|stdClass[] - */ + /** + * @param $oauth_callback | The Callback URL defined within your Application Settings + * @link https://developer.twitter.com/en/docs/apps/callback-urls + * @link https://developer.twitter.com/en/docs/authentication/api-reference/request_token + * @return array|bool|mixed + */ + public function getRequestToken($oauth_callback) { + $resource = 'https://api.twitter.com/oauth/request_token'; + try { + return $this->request($resource, 'POST', ['oauth_callback' => $oauth_callback]); + } catch (Exception $e) { + return false; + } + } + + /** + * Cached HTTP request. + * @param string $resource + * @param array $data + * @param null $cacheExpire + * @return stdClass|stdClass[] + * @throws Exception + * @throws OAuth\Exception + */ public function cachedRequest(string $resource, array $data = [], $cacheExpire = null) { if (!self::$cacheDir) { @@ -410,7 +496,6 @@ public function cachedRequest(string $resource, array $data = [], $cacheExpire = } } - /** * Makes twitter links, @usernames and #hashtags clickable. */ @@ -447,11 +532,7 @@ public static function clickable(stdClass $status): string } } - - /** * An exception generated by Twitter. */ -class Exception extends \Exception -{ -} +class Exception extends \Exception {} From dcae74329b42b1fe0d549e079ad86a42d345704a Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Mon, 15 Nov 2021 09:02:41 -0800 Subject: [PATCH 3/8] Updating Readme for new getRequestToken() method. --- readme.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/readme.md b/readme.md index 6b2088f..4db305d 100644 --- a/readme.md +++ b/readme.md @@ -113,6 +113,27 @@ if (!$twitter->authenticate()) { } ``` +The `getRequestToken()` method allows a Consumer application to obtain an OAuth Request Token to request user authorization: + - Documentation/Use-Cases: https://developer.twitter.com/en/docs/authentication/api-reference/request_token + - Define a Callback URL in your Twitter Application (Required): https://developer.twitter.com/en/docs/apps/callback-urls +```php +# You can Initialize a new Twitter object using only the Consumer Key and Secret +$this->twitter = new Twitter($consumerKey, $consumerSecret); +# Call the getRequestToken() using the callback URL defined in your twitter application. +$response = $this->twitter->getRequestToken('https://localhost.com/twitter-callback-url'); +``` + +Example Response: + +```php +{ + "oauth_token": "x-oFdAAAAAABVLnXXXXXXXX_XXX", + "oauth_token_secret": "A810fifujUZXXXXXXXXXXXXXXXXXXXXX", + "oauth_callback_confirmed": "true" +} +``` + + Other commands -------------- From 57092280e2ce43f72646a2d61f3b05c8b239dec5 Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Mon, 15 Nov 2021 09:03:06 -0800 Subject: [PATCH 4/8] Updating PhpDoc for getRequestToken(). --- src/Twitter.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Twitter.php b/src/Twitter.php index 157f42d..6faaafb 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -439,9 +439,10 @@ public function request(string $resource, string $method, array $data = [], arra /** * @param $oauth_callback | The Callback URL defined within your Application Settings + * @return array|bool|mixed + * @throws OAuth\Exception * @link https://developer.twitter.com/en/docs/apps/callback-urls * @link https://developer.twitter.com/en/docs/authentication/api-reference/request_token - * @return array|bool|mixed */ public function getRequestToken($oauth_callback) { $resource = 'https://api.twitter.com/oauth/request_token'; From 973b8baa7ddbe5675b86b46a6782851368eab52e Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Thu, 16 Dec 2021 13:31:09 -0800 Subject: [PATCH 5/8] Fixing issue with uploading media. --- src/Twitter.php | 640 +++++++++++++++++++++++++----------------------- 1 file changed, 332 insertions(+), 308 deletions(-) diff --git a/src/Twitter.php b/src/Twitter.php index 6faaafb..5634afb 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -21,51 +21,51 @@ */ class Twitter { - public const ME = 1; - public const ME_AND_FRIENDS = 2; - public const REPLIES = 3; - public const RETWEETS = 128; // include retweets? - - private const API_URL = 'https://api.twitter.com/1.1/'; - - /** @var int */ - public static $cacheExpire = '30 minutes'; - - /** @var string */ - public static $cacheDir; - - /** @var array */ - public $httpOptions = [ - CURLOPT_TIMEOUT => 20, - CURLOPT_SSL_VERIFYPEER => 0, - CURLOPT_USERAGENT => 'Twitter for PHP', - ]; - - /** @var OAuth\Consumer */ - private $consumer; - - /** @var OAuth\Token */ - private $token; - - /** - * Creates object using consumer and access keys. - * @throws Exception when CURL extension is not loaded - */ - public function __construct( - string $consumerKey, - string $consumerSecret, - string $accessToken = null, - string $accessTokenSecret = null - ) { - if (!extension_loaded('curl')) { - throw new Exception('PHP extension CURL is not loaded.'); - } - - $this->consumer = new OAuth\Consumer($consumerKey, $consumerSecret); - if ($accessToken && $accessTokenSecret) { - $this->token = new OAuth\Token($accessToken, $accessTokenSecret); - } - } + public const ME = 1; + public const ME_AND_FRIENDS = 2; + public const REPLIES = 3; + public const RETWEETS = 128; // include retweets? + + private const API_URL = 'https://api.twitter.com/1.1/'; + + /** @var int */ + public static $cacheExpire = '30 minutes'; + + /** @var string */ + public static $cacheDir; + + /** @var array */ + public $httpOptions = [ + CURLOPT_TIMEOUT => 20, + CURLOPT_SSL_VERIFYPEER => 0, + CURLOPT_USERAGENT => 'Twitter for PHP', + ]; + + /** @var OAuth\Consumer */ + private $consumer; + + /** @var OAuth\Token */ + private $token; + + /** + * Creates object using consumer and access keys. + * @throws Exception when CURL extension is not loaded + */ + public function __construct( + string $consumerKey, + string $consumerSecret, + string $accessToken = null, + string $accessTokenSecret = null + ) { + if (!extension_loaded('curl')) { + throw new Exception('PHP extension CURL is not loaded.'); + } + + $this->consumer = new OAuth\Consumer($consumerKey, $consumerSecret); + if ($accessToken && $accessTokenSecret) { + $this->token = new OAuth\Token($accessToken, $accessTokenSecret); + } + } /** * Tests if user credentials are valid. @@ -73,19 +73,19 @@ public function __construct( * @throws Exception * @throws OAuth\Exception */ - public function authenticate(): bool - { - try { - $res = $this->request('account/verify_credentials', 'GET'); - return !empty($res->id); - - } catch (Exception $e) { - if ($e->getCode() === 401) { - return false; - } - throw $e; - } - } + public function authenticate(): bool + { + try { + $res = $this->request('account/verify_credentials', 'GET'); + return !empty($res->id); + + } catch (Exception $e) { + if ($e->getCode() === 401) { + return false; + } + throw $e; + } + } /** * Sends message to the Twitter. @@ -97,24 +97,24 @@ public function authenticate(): bool * @throws Exception * @throws OAuth\Exception */ - public function send(string $message, $mediaPath = null, array $options = []): stdClass - { - $mediaIds = []; - foreach ((array) $mediaPath as $item) { - $res = $this->request( - 'https://upload.twitter.com/1.1/media/upload.json', - 'POST', - [], - ['media' => $item] - ); - $mediaIds[] = $res->media_id_string; - } - return $this->request( - 'statuses/update', - 'POST', - $options + ['status' => $message, 'media_ids' => implode(',', $mediaIds) ?: null] - ); - } + public function send(string $message, $mediaPath = null, array $options = []): stdClass + { + $mediaIds = []; + foreach ((array) $mediaPath as $item) { + $res = $this->request( + 'https://upload.twitter.com/1.1/media/upload.json', + 'POST', + [], + ['media' => $item] + ); + $mediaIds[] = $res->media_id_string; + } + return $this->request( + 'statuses/update', + 'POST', + $options + ['status' => $message, 'media_ids' => implode(',', $mediaIds) ?: null] + ); + } /** * Sends a direct message to the specified user. @@ -125,20 +125,20 @@ public function send(string $message, $mediaPath = null, array $options = []): s * @throws Exception * @throws OAuth\Exception */ - public function sendDirectMessage(string $username, string $message): stdClass - { - return $this->request( - 'direct_messages/events/new', - 'JSONPOST', - ['event' => [ - 'type' => 'message_create', - 'message_create' => [ - 'target' => ['recipient_id' => $this->loadUserInfo($username)->id_str], - 'message_data' => ['text' => $message], - ], - ]] - ); - } + public function sendDirectMessage(string $username, string $message): stdClass + { + return $this->request( + 'direct_messages/events/new', + 'JSONPOST', + ['event' => [ + 'type' => 'message_create', + 'message_create' => [ + 'target' => ['recipient_id' => $this->loadUserInfo($username)->id_str], + 'message_data' => ['text' => $message], + ], + ]] + ); + } /** * Follows a user on Twitter. @@ -148,10 +148,10 @@ public function sendDirectMessage(string $username, string $message): stdClass * @throws Exception * @throws OAuth\Exception */ - public function follow(string $username): stdClass - { - return $this->request('friendships/create', 'POST', ['screen_name' => $username]); - } + public function follow(string $username): stdClass + { + return $this->request('friendships/create', 'POST', ['screen_name' => $username]); + } /** * Returns the most recent statuses. @@ -163,22 +163,22 @@ public function follow(string $username): stdClass * @throws Exception * @throws OAuth\Exception */ - public function load(int $flags = self::ME, int $count = 20, array $data = null): array - { - static $timelines = [ - self::ME => 'user_timeline', - self::ME_AND_FRIENDS => 'home_timeline', - self::REPLIES => 'mentions_timeline', - ]; - if (!isset($timelines[$flags & 3])) { - throw new \InvalidArgumentException; - } - - return $this->cachedRequest('statuses/' . $timelines[$flags & 3], (array) $data + [ - 'count' => $count, - 'include_rts' => $flags & self::RETWEETS ? 1 : 0, - ]); - } + public function load(int $flags = self::ME, int $count = 20, array $data = null): array + { + static $timelines = [ + self::ME => 'user_timeline', + self::ME_AND_FRIENDS => 'home_timeline', + self::REPLIES => 'mentions_timeline', + ]; + if (!isset($timelines[$flags & 3])) { + throw new \InvalidArgumentException; + } + + return $this->cachedRequest('statuses/' . $timelines[$flags & 3], (array) $data + [ + 'count' => $count, + 'include_rts' => $flags & self::RETWEETS ? 1 : 0, + ]); + } /** * Returns information of a given user. @@ -188,10 +188,10 @@ public function load(int $flags = self::ME, int $count = 20, array $data = null) * @throws Exception * @throws OAuth\Exception */ - public function loadUserInfo(string $username): stdClass - { - return $this->cachedRequest('users/show', ['screen_name' => $username]); - } + public function loadUserInfo(string $username): stdClass + { + return $this->cachedRequest('users/show', ['screen_name' => $username]); + } /** * Returns information of a given user by id. @@ -201,10 +201,10 @@ public function loadUserInfo(string $username): stdClass * @throws Exception * @throws OAuth\Exception */ - public function loadUserInfoById(string $id): stdClass - { - return $this->cachedRequest('users/show', ['user_id' => $id]); - } + public function loadUserInfoById(string $id): stdClass + { + return $this->cachedRequest('users/show', ['user_id' => $id]); + } /** * Returns IDs of followers of a given user. @@ -217,18 +217,18 @@ public function loadUserInfoById(string $id): stdClass * @throws Exception * @throws OAuth\Exception */ - public function loadUserFollowers( - string $username, - int $count = 5000, - int $cursor = -1, - $cacheExpiry = null - ): stdClass { - return $this->cachedRequest('followers/ids', [ - 'screen_name' => $username, - 'count' => $count, - 'cursor' => $cursor, - ], $cacheExpiry); - } + public function loadUserFollowers( + string $username, + int $count = 5000, + int $cursor = -1, + $cacheExpiry = null + ): stdClass { + return $this->cachedRequest('followers/ids', [ + 'screen_name' => $username, + 'count' => $count, + 'cursor' => $cursor, + ], $cacheExpiry); + } /** * Returns list of followers of a given user. @@ -241,18 +241,18 @@ public function loadUserFollowers( * @throws Exception * @throws OAuth\Exception */ - public function loadUserFollowersList( - string $username, - int $count = 200, - int $cursor = -1, - $cacheExpiry = null - ): stdClass { - return $this->cachedRequest('followers/list', [ - 'screen_name' => $username, - 'count' => $count, - 'cursor' => $cursor, - ], $cacheExpiry); - } + public function loadUserFollowersList( + string $username, + int $count = 200, + int $cursor = -1, + $cacheExpiry = null + ): stdClass { + return $this->cachedRequest('followers/list', [ + 'screen_name' => $username, + 'count' => $count, + 'cursor' => $cursor, + ], $cacheExpiry); + } /** * Destroys status. @@ -261,11 +261,11 @@ public function loadUserFollowersList( * @throws Exception * @throws OAuth\Exception */ - public function destroy($id) - { - $res = $this->request("statuses/destroy/$id", 'POST', ['id' => $id]); - return $res->id ?: false; - } + public function destroy($id) + { + $res = $this->request("statuses/destroy/$id", 'POST', ['id' => $id]); + return $res->id ?: false; + } /** * Retrieves a single status. @@ -274,11 +274,11 @@ public function destroy($id) * @throws Exception * @throws OAuth\Exception */ - public function get($id) - { - $res = $this->request("statuses/show/$id", 'GET'); - return $res; - } + public function get($id) + { + $res = $this->request("statuses/show/$id", 'GET'); + return $res; + } /** * Returns tweets that match a specified query. @@ -289,11 +289,11 @@ public function get($id) * @throws Exception * @throws OAuth\Exception */ - public function search($query, bool $full = false) - { - $res = $this->request('search/tweets', 'GET', is_array($query) ? $query : ['q' => $query]); - return $full ? $res : $res->statuses; - } + public function search($query, bool $full = false) + { + $res = $this->request('search/tweets', 'GET', is_array($query) ? $query : ['q' => $query]); + return $full ? $res : $res->statuses; + } /** * Retrieves the top 50 trending topics for a specific WOEID. @@ -302,10 +302,10 @@ public function search($query, bool $full = false) * @throws Exception * @throws OAuth\Exception */ - public function getTrends(int $WOEID): array - { - return $this->request("trends/place.json?id=$WOEID", 'GET'); - } + public function getTrends(int $WOEID): array + { + return $this->request("trends/place.json?id=$WOEID", 'GET'); + } /** * Process HTTP request. @@ -317,68 +317,68 @@ public function getTrends(int $WOEID): array * @throws Exception * @throws OAuth\Exception */ - public function request(string $resource, string $method, array $data = [], array $files = []) - { - if (!strpos($resource, '://')) { - if (!strpos($resource, '.')) { - $resource .= '.json'; - } - $resource = self::API_URL . $resource; - } - - foreach ($data as $key => $val) { - if ($val === null) { - unset($data[$key]); - } - } - - foreach ($files as $key => $file) { - if (!is_file($file)) { - throw new Exception("Cannot read the file $file. Check if file exists on disk and check its permissions."); - } - $data[$key] = new \CURLFile($file); - } - - $headers = ['Expect:']; - - if ($method === 'JSONPOST') { - $method = 'POST'; - $data = json_encode($data); - $headers[] = 'Content-Type: application/json'; - - } elseif (($method === 'GET' || $method === 'DELETE') && $data) { - $resource .= '?' . http_build_query($data, '', '&'); - } - - $request = OAuth\Request::from_consumer_and_token($this->consumer, $this->token, $method, $resource); - $request->sign_request(new OAuth\SignatureMethod_HMAC_SHA1, $this->consumer, $this->token); - $headers[] = $request->to_header(); - - $options = [ - CURLOPT_URL => $resource, - CURLOPT_HEADER => false, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => $headers, - ] + $this->httpOptions; - - if ($method === 'POST') { - $options += [ - CURLOPT_POST => true, - CURLOPT_POSTFIELDS => $data, - CURLOPT_SAFE_UPLOAD => true, - ]; - } elseif ($method === 'DELETE') { - $options += [ - CURLOPT_CUSTOMREQUEST => 'DELETE', - ]; - } - - $curl = curl_init(); - curl_setopt_array($curl, $options); + public function request(string $resource, string $method, array $data = [], array $files = []) + { + if (!strpos($resource, '://')) { + if (!strpos($resource, '.')) { + $resource .= '.json'; + } + $resource = self::API_URL . $resource; + } + + foreach ($data as $key => $val) { + if ($val === null) { + unset($data[$key]); + } + } + + foreach ($files as $key => $file) { + if (!is_file($file)) { + throw new Exception("Cannot read the file $file. Check if file exists on disk and check its permissions."); + } + $data[$key] = new \CURLFile($file); + } + + $headers = ['Expect:']; + + if ($method === 'JSONPOST') { + $method = 'POST'; + $data = json_encode($data); + $headers[] = 'Content-Type: application/json'; + + } elseif (($method === 'GET' || $method === 'POST') && $data) { + $resource .= '?' . http_build_query($data, '', '&'); + } + + $request = OAuth\Request::from_consumer_and_token($this->consumer, $this->token, $method, $resource); + $request->sign_request(new OAuth\SignatureMethod_HMAC_SHA1, $this->consumer, $this->token); + $headers[] = $request->to_header(); + + $options = [ + CURLOPT_URL => $resource, + CURLOPT_HEADER => false, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => $headers, + ] + $this->httpOptions; + + if ($method === 'POST') { + $options += [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $data, + CURLOPT_SAFE_UPLOAD => true, + ]; + } elseif ($method === 'DELETE') { + $options += [ + CURLOPT_CUSTOMREQUEST => 'DELETE', + ]; + } + + $curl = curl_init(); + curl_setopt_array($curl, $options); /** * The result of the request (Raw Body) */ - $result = curl_exec($curl); + $result = curl_exec($curl); /** * Get the Content-Type from the Response; */ @@ -387,27 +387,27 @@ public function request(string $resource, string $method, array $data = [], arra * Get the Response Code from the Request. */ $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); - if (curl_errno($curl)) { - throw new Exception('Server error: ' . curl_error($curl)); - } + if (curl_errno($curl)) { + throw new Exception('Server error: ' . curl_error($curl)); + } /** * If JSON was returned, decode and return. */ - if (strpos($contentType, 'application/json') !== false) { - $payload = @json_decode($result, false, 128, JSON_BIGINT_AS_STRING); // intentionally @ - if ($payload === false) { - throw new Exception('Invalid server response'); - } - } - - if ($code >= 400) { - throw new Exception( - $payload->errors[0]->message ?? "Server error #$code with answer $result", - $code - ); - } elseif ($code === 204) { - $payload = true; - } + if (strpos($contentType, 'application/json') !== false) { + $payload = @json_decode($result, false, 128, JSON_BIGINT_AS_STRING); // intentionally @ + if ($payload === false) { + throw new Exception('Invalid server response'); + } + } + + if ($code >= 400) { + throw new Exception( + $payload->errors[0]->message ?? "Server error #$code with answer $result", + $code + ); + } elseif ($code === 204) { + $payload = true; + } /** * If the payload isn't null or undefined. */ @@ -435,7 +435,7 @@ public function request(string $resource, string $method, array $data = [], arra else { throw new Exception('Invalid server response (Not Valid)'); } - } + } /** * @param $oauth_callback | The Callback URL defined within your Application Settings @@ -453,6 +453,30 @@ public function getRequestToken($oauth_callback) { } } + /** + * @param $oauth_token + * @param $oauth_verifier + * @return array|bool|mixed + * @throws OAuth\Exception + */ + public function getAccessToken($oauth_token, $oauth_verifier) { + $resource = "https://api.twitter.com/oauth/access_token"; + try { + return $this->request($resource, 'POST', ['oauth_verifier' => $oauth_verifier, 'oauth_token' => $oauth_token]); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + public function invalidateToken($access_token, $access_token_secret) { + $resource = "https://api.twitter.com/oauth/invalidate_token"; + try { + return $this->request($resource, 'POST', ['access_token' => $access_token, 'access_token_secret' => $access_token_secret]); + } catch (Exception $e) { + return $e->getMessage(); + } + } + /** * Cached HTTP request. * @param string $resource @@ -462,75 +486,75 @@ public function getRequestToken($oauth_callback) { * @throws Exception * @throws OAuth\Exception */ - public function cachedRequest(string $resource, array $data = [], $cacheExpire = null) - { - if (!self::$cacheDir) { - return $this->request($resource, 'GET', $data); - } - if ($cacheExpire === null) { - $cacheExpire = self::$cacheExpire; - } - - $cacheFile = self::$cacheDir - . '/twitter.' - . md5($resource . json_encode($data) . serialize([$this->consumer, $this->token])) - . '.json'; - - $cache = @json_decode((string) @file_get_contents($cacheFile)); // intentionally @ - $expiration = is_string($cacheExpire) - ? strtotime($cacheExpire) - time() - : $cacheExpire; - if ($cache && @filemtime($cacheFile) + $expiration > time()) { // intentionally @ - return $cache; - } - - try { - $payload = $this->request($resource, 'GET', $data); - file_put_contents($cacheFile, json_encode($payload)); - return $payload; - - } catch (Exception $e) { - if ($cache) { - return $cache; - } - throw $e; - } - } - - /** - * Makes twitter links, @usernames and #hashtags clickable. - */ - public static function clickable(stdClass $status): string - { - $all = []; - foreach ($status->entities->hashtags as $item) { - $all[$item->indices[0]] = ["https://twitter.com/search?q=%23$item->text", "#$item->text", $item->indices[1]]; - } - foreach ($status->entities->urls as $item) { - if (!isset($item->expanded_url)) { - $all[$item->indices[0]] = [$item->url, $item->url, $item->indices[1]]; - } else { - $all[$item->indices[0]] = [$item->expanded_url, $item->display_url, $item->indices[1]]; - } - } - foreach ($status->entities->user_mentions as $item) { - $all[$item->indices[0]] = ["https://twitter.com/$item->screen_name", "@$item->screen_name", $item->indices[1]]; - } - if (isset($status->entities->media)) { - foreach ($status->entities->media as $item) { - $all[$item->indices[0]] = [$item->url, $item->display_url, $item->indices[1]]; - } - } - - krsort($all); - $s = $status->full_text ?? $status->text; - foreach ($all as $pos => $item) { - $s = iconv_substr($s, 0, $pos, 'UTF-8') - . '' . htmlspecialchars($item[1]) . '' - . iconv_substr($s, $item[2], iconv_strlen($s, 'UTF-8'), 'UTF-8'); - } - return $s; - } + public function cachedRequest(string $resource, array $data = [], $cacheExpire = null) + { + if (!self::$cacheDir) { + return $this->request($resource, 'GET', $data); + } + if ($cacheExpire === null) { + $cacheExpire = self::$cacheExpire; + } + + $cacheFile = self::$cacheDir + . '/twitter.' + . md5($resource . json_encode($data) . serialize([$this->consumer, $this->token])) + . '.json'; + + $cache = @json_decode((string) @file_get_contents($cacheFile)); // intentionally @ + $expiration = is_string($cacheExpire) + ? strtotime($cacheExpire) - time() + : $cacheExpire; + if ($cache && @filemtime($cacheFile) + $expiration > time()) { // intentionally @ + return $cache; + } + + try { + $payload = $this->request($resource, 'GET', $data); + file_put_contents($cacheFile, json_encode($payload)); + return $payload; + + } catch (Exception $e) { + if ($cache) { + return $cache; + } + throw $e; + } + } + + /** + * Makes twitter links, @usernames and #hashtags clickable. + */ + public static function clickable(stdClass $status): string + { + $all = []; + foreach ($status->entities->hashtags as $item) { + $all[$item->indices[0]] = ["https://twitter.com/search?q=%23$item->text", "#$item->text", $item->indices[1]]; + } + foreach ($status->entities->urls as $item) { + if (!isset($item->expanded_url)) { + $all[$item->indices[0]] = [$item->url, $item->url, $item->indices[1]]; + } else { + $all[$item->indices[0]] = [$item->expanded_url, $item->display_url, $item->indices[1]]; + } + } + foreach ($status->entities->user_mentions as $item) { + $all[$item->indices[0]] = ["https://twitter.com/$item->screen_name", "@$item->screen_name", $item->indices[1]]; + } + if (isset($status->entities->media)) { + foreach ($status->entities->media as $item) { + $all[$item->indices[0]] = [$item->url, $item->display_url, $item->indices[1]]; + } + } + + krsort($all); + $s = $status->full_text ?? $status->text; + foreach ($all as $pos => $item) { + $s = iconv_substr($s, 0, $pos, 'UTF-8') + . '' . htmlspecialchars($item[1]) . '' + . iconv_substr($s, $item[2], iconv_strlen($s, 'UTF-8'), 'UTF-8'); + } + return $s; + } } /** From 44c613ebfa98b3e964572d3f35a2650e36c7694f Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Thu, 16 Dec 2021 13:32:31 -0800 Subject: [PATCH 6/8] Updating InvalidateAccessToken method. --- src/Twitter.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Twitter.php b/src/Twitter.php index 5634afb..7e6debd 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -468,10 +468,14 @@ public function getAccessToken($oauth_token, $oauth_verifier) { } } - public function invalidateToken($access_token, $access_token_secret) { - $resource = "https://api.twitter.com/oauth/invalidate_token"; + /** + * @return array|bool|mixed|string + * @throws OAuth\Exception + */ + public function invalidateAccessToken() { + $resource = "https://api.twitter.com/1.1/oauth/invalidate_token"; try { - return $this->request($resource, 'POST', ['access_token' => $access_token, 'access_token_secret' => $access_token_secret]); + return $this->request($resource, 'POST', ['access_token' => $this->token->key, 'access_token_secret' => $this->token->secret]); } catch (Exception $e) { return $e->getMessage(); } From a9ae2b9583776ab2f011386b59966926e03eb245 Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Thu, 10 Feb 2022 17:48:36 -0800 Subject: [PATCH 7/8] Adjusting Twitter AuthPost to put params in URL, adding .idea to gitignore. --- .gitignore | 1 + src/Twitter.php | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index de4a392..e0d383d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +/.idea /vendor /composer.lock diff --git a/src/Twitter.php b/src/Twitter.php index 7e6debd..5f05ffd 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -348,6 +348,8 @@ public function request(string $resource, string $method, array $data = [], arra } elseif (($method === 'GET' || $method === 'POST') && $data) { $resource .= '?' . http_build_query($data, '', '&'); + } elseif ($method == 'AUTHPOST') { + $method = 'POST'; } $request = OAuth\Request::from_consumer_and_token($this->consumer, $this->token, $method, $resource); @@ -445,10 +447,11 @@ public function request(string $resource, string $method, array $data = [], arra * @link https://developer.twitter.com/en/docs/authentication/api-reference/request_token */ public function getRequestToken($oauth_callback) { - $resource = 'https://api.twitter.com/oauth/request_token'; + $resource = 'https://api.twitter.com/oauth/request_token?oauth_callback=' . urlencode($oauth_callback); try { - return $this->request($resource, 'POST', ['oauth_callback' => $oauth_callback]); + return $this->request($resource, 'AUTHPOST'); } catch (Exception $e) { + print($e->getMessage()); return false; } } From bbbe15760cd27121fd4e1be3dfe54c61ecae0dbc Mon Sep 17 00:00:00 2001 From: bijanmmarkes Date: Fri, 25 Feb 2022 14:43:54 -0800 Subject: [PATCH 8/8] Cleanup and throwing exceptions vs returning message. --- src/Twitter.php | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Twitter.php b/src/Twitter.php index 5f05ffd..a3cd1ef 100644 --- a/src/Twitter.php +++ b/src/Twitter.php @@ -440,19 +440,28 @@ public function request(string $resource, string $method, array $data = [], arra } /** + * @link https://developer.twitter.com/en/docs/apps/callback-urls + * @link https://developer.twitter.com/en/docs/authentication/api-reference/request_token * @param $oauth_callback | The Callback URL defined within your Application Settings * @return array|bool|mixed + * @throws Exception * @throws OAuth\Exception - * @link https://developer.twitter.com/en/docs/apps/callback-urls - * @link https://developer.twitter.com/en/docs/authentication/api-reference/request_token */ public function getRequestToken($oauth_callback) { $resource = 'https://api.twitter.com/oauth/request_token?oauth_callback=' . urlencode($oauth_callback); try { - return $this->request($resource, 'AUTHPOST'); + /** + * This is a fault in Twitter API requiring it to be sent as a POST + * While at the same time using Query Parameters. + * Otherwise the callback is not appended, and it goes with the default callback + * set within Twitter Dev portal, meaning you can only use one callback per Environment. + */ + return $this->request( + $resource, + 'AUTHPOST' + ); } catch (Exception $e) { - print($e->getMessage()); - return false; + throw new Exception($e->getMessage(), $e->getCode()); } } @@ -460,27 +469,49 @@ public function getRequestToken($oauth_callback) { * @param $oauth_token * @param $oauth_verifier * @return array|bool|mixed + * @throws Exception * @throws OAuth\Exception */ public function getAccessToken($oauth_token, $oauth_verifier) { $resource = "https://api.twitter.com/oauth/access_token"; try { - return $this->request($resource, 'POST', ['oauth_verifier' => $oauth_verifier, 'oauth_token' => $oauth_token]); + return $this->request( + $resource, + 'POST', + [ + 'oauth_verifier' => $oauth_verifier, + 'oauth_token' => $oauth_token + ] + ); } catch (Exception $e) { - return $e->getMessage(); + throw new Exception($e->getMessage(), $e->getCode()); } } /** * @return array|bool|mixed|string + * @throws Exception * @throws OAuth\Exception + * Twitter Code: 89 = Invalid or expired token. (HTTP 401) */ public function invalidateAccessToken() { $resource = "https://api.twitter.com/1.1/oauth/invalidate_token"; try { - return $this->request($resource, 'POST', ['access_token' => $this->token->key, 'access_token_secret' => $this->token->secret]); + return $this->request( + $resource, + 'POST', + [ + 'access_token' => $this->token->key, + 'access_token_secret' => $this->token->secret + ] + ); } catch (Exception $e) { - return $e->getMessage(); + /** + * If the token has already been invalidated, or it's expired + * Returns HTTP 401, use this to know when it is invalid or expired. + * Any other error means that something unexpected happened. (404, 500, etc) + */ + throw new Exception($e->getMessage(), $e->getCode()); } }