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/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/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
--------------
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..a3cd1ef 100644
--- a/src/Twitter.php
+++ b/src/Twitter.php
@@ -14,431 +14,588 @@
*/
namespace DG\Twitter;
-
use stdClass;
-
/**
* Twitter API.
*/
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);
- }
- }
-
-
- /**
- * Tests if user credentials are valid.
- * @throws 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;
- }
- }
-
-
- /**
- * 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
- */
- 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.
- * https://dev.twitter.com/rest/reference/post/direct_messages/new
- * @throws 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],
- ],
- ]]
- );
- }
-
-
- /**
- * Follows a user on Twitter.
- * https://dev.twitter.com/rest/reference/post/friendships/create
- * @throws 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
- */
- 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.
- * https://dev.twitter.com/rest/reference/get/users/show
- * @throws 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
- */
- 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
- */
- 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.
- * 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
- {
- return $this->cachedRequest('followers/list', [
- 'screen_name' => $username,
- 'count' => $count,
- 'cursor' => $cursor,
- ], $cacheExpiry);
- }
-
-
- /**
- * Destroys status.
- * @param int|string $id status to be destroyed
- * @throws 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
- */
- 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[]
- */
- 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
- */
- 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
- */
- 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);
- $result = curl_exec($curl);
- if (curl_errno($curl)) {
- throw new Exception('Server error: ' . curl_error($curl));
- }
-
- if (strpos(curl_getinfo($curl, CURLINFO_CONTENT_TYPE), '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(isset($payload->errors[0]->message)
- ? $payload->errors[0]->message
- : "Server error #$code with answer $result",
- $code
- );
- } elseif ($code === 204) {
- $payload = true;
- }
-
- return $payload;
- }
-
-
- /**
- * Cached HTTP request.
- * @return stdClass|stdClass[]
- */
- 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 = isset($status->full_text) ? $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 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.
+ * @return bool
+ * @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;
+ }
+ }
+
+ /**
+ * 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 = [];
+ 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.
+ * 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(
+ '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.
+ * 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)
+ * @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 = [
+ 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.
+ * 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
+ * @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
+ * @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,
+ 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.
+ * 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,
+ int $cursor = -1,
+ $cacheExpiry = null
+ ): stdClass {
+ return $this->cachedRequest('followers/list', [
+ 'screen_name' => $username,
+ 'count' => $count,
+ 'cursor' => $cursor,
+ ], $cacheExpiry);
+ }
+
+ /**
+ * 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
+ * @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
+ * @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
+ * @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 $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, '://')) {
+ 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, '', '&');
+ } elseif ($method == 'AUTHPOST') {
+ $method = 'POST';
+ }
+
+ $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);
+ /**
+ * 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 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 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)');
+ }
+ }
+
+ /**
+ * @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
+ */
+ public function getRequestToken($oauth_callback) {
+ $resource = 'https://api.twitter.com/oauth/request_token?oauth_callback=' . urlencode($oauth_callback);
+ try {
+ /**
+ * 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) {
+ throw new Exception($e->getMessage(), $e->getCode());
+ }
+ }
+
+ /**
+ * @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
+ ]
+ );
+ } catch (Exception $e) {
+ 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
+ ]
+ );
+ } catch (Exception $e) {
+ /**
+ * 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());
+ }
+ }
+
+ /**
+ * 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) {
+ 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;
+ }
}
-
-
/**
* An exception generated by Twitter.
*/
-class Exception extends \Exception
-{
-}
+class Exception extends \Exception {}