From d58e14aa4364562be1bfa4190127345b3c82767a Mon Sep 17 00:00:00 2001 From: Jan Tojnar Date: Wed, 3 Apr 2019 20:30:44 +0200 Subject: [PATCH] spouts: Port Twitter spouts to Guzzle This concludes our effort to port selfoss to Guzzle. --- NEWS.md | 1 + README.md | 2 - composer.json | 2 +- composer.lock | 107 +++++++++++++------------- src/spouts/twitter/hometimeline.php | 24 +----- src/spouts/twitter/listtimeline.php | 31 ++------ src/spouts/twitter/usertimeline.php | 113 ++++++++++++++++++++++------ 7 files changed, 152 insertions(+), 128 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5ad4980d53..770edf64f3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -47,6 +47,7 @@ ### Other changes - Removed broken instapaper scraping from Reddit spout ([#1033](https://github.com/SSilence/selfoss/pull/1033)) - RSS feed will be fetched more reliably ([#1052](https://github.com/SSilence/selfoss/pull/1052)) +- Guzzle is now used for Twitter as well, allowing users to [install certificates](https://github.com/SSilence/selfoss/issues/1099#issuecomment-477112598) on outdated hosts easily. ([#1102](https://github.com/SSilence/selfoss/pull/1102)) - More of user interface is now translatable ([#1054](https://github.com/SSilence/selfoss/pull/1054)) - Open Sans font is no longer bundled, resulting in smaller installations. Additionally, `use_system_font` option was removed. The typeface is still set as the default font family, so if you want to use it, install it to your devices. If you want to use a different typeface, add `body { font-family: 'Foo Face'; }` to your `user.css`. ([#1072](https://github.com/SSilence/selfoss/pull/1072)) - The file name of exported sources now includes a timestamp ([#1078](https://github.com/SSilence/selfoss/pull/1078)) diff --git a/README.md b/README.md index fd4036d22a..646f3d5ae2 100644 --- a/README.md +++ b/README.md @@ -98,12 +98,10 @@ Special thanks to the great programmers of this libraries which will be used in * [WideImage](http://wideimage.sourceforge.net/) * [htmLawed](http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed/) * [PHP Universal Feed Generator](https://github.com/ajaxray/FeedWriter) -* [twitteroauth](https://github.com/abraham/twitteroauth) * [Elphin IcoFileLoader](https://github.com/lordelph/icofileloader) * [jQuery hotkeys](https://github.com/tzuryby/jquery.hotkeys) * [Spectrum Colorpicker](https://github.com/bgrins/spectrum) * [jQuery custom content scroller](http://manos.malihu.gr/jquery-custom-content-scroller/) -* [twitter oauth library](https://github.com/abraham/twitteroauth) * [FullTextRSS](http://help.fivefilters.org/customer/portal/articles/223153-site-patterns) * [Graby](https://github.com/j0k3r/graby) diff --git a/composer.json b/composer.json index 4463464f4e..17584cea6b 100644 --- a/composer.json +++ b/composer.json @@ -4,12 +4,12 @@ "type": "project", "require": { "php": ">= 5.6", - "abraham/twitteroauth": "^1.0", "bcosca/fatfree-core": "dev-selfoss#cc3aa6af9a038dd9d0e136f42d9d18a02e077f39", "danielstjules/stringy": "^3.1", "fossar/tcpdf-parser": "^6.2", "fossar/guzzle-transcoder": "^0.1.0", "guzzlehttp/guzzle": "^6.3", + "guzzlehttp/oauth-subscriber": "^0.3.0", "guzzlehttp/psr7": "^1.5", "fossar/htmlawed": "^1.2.4.1", "j0k3r/graby": "2.0.0-alpha.0", diff --git a/composer.lock b/composer.lock index 4d8245fec3..65ed588a67 100644 --- a/composer.lock +++ b/composer.lock @@ -4,62 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8d4a7028c1afe5519ae6de9a8cc0370e", + "content-hash": "32c6a1b1a3227023e875bdc5943f9067", "packages": [ - { - "name": "abraham/twitteroauth", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/abraham/twitteroauth.git", - "reference": "ed70c5de0ea33439fde124d0d7109460bbb59a71" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/abraham/twitteroauth/zipball/ed70c5de0ea33439fde124d0d7109460bbb59a71", - "reference": "ed70c5de0ea33439fde124d0d7109460bbb59a71", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": "^5.6 || ^7.0 || ^7.1 || ^7.2" - }, - "require-dev": { - "phpmd/phpmd": "~2.6", - "phpunit/phpunit": "~5.7", - "squizlabs/php_codesniffer": "~3.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Abraham\\TwitterOAuth\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Abraham Williams", - "email": "abraham@abrah.am", - "homepage": "https://abrah.am", - "role": "Developer" - } - ], - "description": "The most popular PHP library for use with the Twitter OAuth REST API.", - "homepage": "https://twitteroauth.com", - "keywords": [ - "Twitter API", - "Twitter oAuth", - "api", - "oauth", - "rest", - "social", - "twitter" - ], - "time": "2019-01-20T17:52:37+00:00" - }, { "name": "bcosca/fatfree-core", "version": "dev-selfoss", @@ -530,6 +476,57 @@ ], "time": "2018-04-22T15:46:56+00:00" }, + { + "name": "guzzlehttp/oauth-subscriber", + "version": "0.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/oauth-subscriber.git", + "reference": "04960cdef3cd80ea401d6b0ca8b3e110e9bf12cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/oauth-subscriber/zipball/04960cdef3cd80ea401d6b0ca8b3e110e9bf12cf", + "reference": "04960cdef3cd80ea401d6b0ca8b3e110e9bf12cf", + "shasum": "" + }, + "require": { + "guzzlehttp/guzzle": "~6.0", + "php": ">=5.5.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Subscriber\\Oauth\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + } + ], + "description": "Guzzle OAuth 1.0 subscriber", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "Guzzle", + "oauth" + ], + "time": "2015-08-15T19:44:28+00:00" + }, { "name": "guzzlehttp/promises", "version": "v1.3.1", diff --git a/src/spouts/twitter/hometimeline.php b/src/spouts/twitter/hometimeline.php index d9c87df93d..eebc25ec56 100644 --- a/src/spouts/twitter/hometimeline.php +++ b/src/spouts/twitter/hometimeline.php @@ -2,8 +2,6 @@ namespace spouts\twitter; -use Abraham\TwitterOAuth\TwitterOAuth; - /** * Spout for fetching the twitter timeline of your twitter account * @@ -58,27 +56,9 @@ class hometimeline extends \spouts\twitter\usertimeline { * @return void */ public function load(array $params) { - $twitter = new TwitterOAuth($params['consumer_key'], $params['consumer_secret'], $params['access_key'], $params['access_secret']); - $timeline = $twitter->get('statuses/home_timeline', [ - 'include_rts' => 1, - 'count' => 50, - 'tweet_mode' => 'extended', - ]); - - if (isset($timeline->errors)) { - $errors = ''; - - foreach ($timeline->errors as $error) { - $errors .= $error->message . "\n"; - } - - throw new \Exception($errors); - } + $this->client = self::getHttpClient($params['consumer_key'], $params['consumer_secret'], $params['access_key'], $params['access_secret']); - if (!is_array($timeline)) { - throw new \Exception('invalid twitter response'); - } - $this->items = $timeline; + $this->items = $this->fetchTwitterTimeline('statuses/home_timeline'); $this->htmlUrl = 'https://twitter.com/'; diff --git a/src/spouts/twitter/listtimeline.php b/src/spouts/twitter/listtimeline.php index 6a886fc6d0..5ee82a4526 100644 --- a/src/spouts/twitter/listtimeline.php +++ b/src/spouts/twitter/listtimeline.php @@ -2,8 +2,6 @@ namespace spouts\twitter; -use Abraham\TwitterOAuth\TwitterOAuth; - /** * Spout for fetching a twitter list * @@ -69,31 +67,12 @@ public function __construct() { * @return void */ public function load(array $params) { - $access_token_used = !empty($params['access_token']) && !empty($params['access_token_secret']); - $twitter = new TwitterOAuth($params['consumer_key'], $params['consumer_secret'], $access_token_used ? $params['access_token'] : null, $access_token_used ? $params['access_token_secret'] : null); - $timeline = $twitter->get('lists/statuses', - [ - 'slug' => $params['slug'], - 'owner_screen_name' => $params['owner_screen_name'], - 'include_rts' => 1, - 'count' => 50, - 'tweet_mode' => 'extended', - ]); - - if (isset($timeline->errors)) { - $errors = ''; - - foreach ($timeline->errors as $error) { - $errors .= $error->message . "\n"; - } - - throw new \Exception($errors); - } + $this->client = self::getHttpClient($params['consumer_key'], $params['consumer_secret'], $params['access_token'], $params['access_token_secret']); - if (!is_array($timeline)) { - throw new \Exception('invalid twitter response'); - } - $this->items = $timeline; + $this->items = $this->fetchTwitterTimeline('lists/statuses', [ + 'slug' => $params['slug'], + 'owner_screen_name' => $params['owner_screen_name'], + ]); $this->htmlUrl = 'https://twitter.com/' . urlencode($params['owner_screen_name']); diff --git a/src/spouts/twitter/usertimeline.php b/src/spouts/twitter/usertimeline.php index f1bea1e25a..511f4e72cb 100644 --- a/src/spouts/twitter/usertimeline.php +++ b/src/spouts/twitter/usertimeline.php @@ -2,7 +2,10 @@ namespace spouts\twitter; -use Abraham\TwitterOAuth\TwitterOAuth; +use GuzzleHttp; +use GuzzleHttp\Exception\BadResponseException; +use GuzzleHttp\Subscriber\Oauth\Oauth1; +use helpers\WebClient; use stdClass; /** @@ -64,6 +67,90 @@ class usertimeline extends \spouts\spout { /** @var string URL of the source */ protected $htmlUrl = ''; + /** @var ?GuzzleHttp\Client HTTP client configured with Twitter OAuth support */ + protected $client = null; + + /** + * Provide a HTTP client for use by spouts + * + * @param string $consumerKey + * @param string $consumerSecret + * @param string $accessToken + * @param string $accessTokenSecret + * + * @return GuzzleHttp\Client + */ + public static function getHttpClient($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret) { + $access_token_used = !empty($accessToken) && !empty($accessTokenSecret); + + $oldClient = WebClient::getHttpClient(); + $config = $oldClient->getConfig(); + + $config['base_uri'] = 'https://api.twitter.com/1.1/'; + $config['auth'] = 'oauth'; + $middleware = new Oauth1([ + 'consumer_key' => $consumerKey, + 'consumer_secret' => $consumerSecret, + 'token' => $access_token_used ? $accessToken : '', + 'token_secret' => $access_token_used ? $accessTokenSecret : '', + ]); + $config['handler'] = clone $config['handler']; // we do not want to contaminate other spouts + $config['handler']->push($middleware); + + return new GuzzleHttp\Client($config); + } + + /** + * Fetch timeline from Twitter API. + * + * Assumes client property is initialized to Guzzle client configured to access Twitter. + * + * @param string $endpoint API endpoint to use + * @param array $params extra query arguments to pass to the API call + * + * @throws Exception when API request fails + * @throws GuzzleHttp\Exception\RequestException when HTTP request fails for API-unrelated reasons + * + * @return stdClass[] + */ + protected function fetchTwitterTimeline($endpoint, array $params = []) { + if (!isset($this->client)) { + throw new \Exception('Twitter client was not initialized.'); + } + + try { + $response = $this->client->get("$endpoint.json", [ + 'query' => array_merge([ + 'include_rts' => 1, + 'count' => 50, + 'tweet_mode' => 'extended', + ], $params), + ]); + + $timeline = json_decode((string) $response->getBody()); + + if (!is_array($timeline)) { + throw new \Exception('Invalid twitter response'); + } + + return $timeline; + } catch (BadResponseException $e) { + if ($e->hasResponse()) { + $body = json_decode((string) $e->getResponse()->getBody()); + + if (isset($body->errors)) { + $errors = implode("\n", array_map(function($error) { + return $error->message; + }, $body->errors)); + + throw new \Exception($errors, $e->getCode(), $e); + } + } + + throw $e; + } + } + // // Iterator Interface // @@ -145,30 +232,12 @@ public function valid() { * @return void */ public function load(array $params) { - $access_token_used = !empty($params['access_token']) && !empty($params['access_token_secret']); - $twitter = new TwitterOAuth($params['consumer_key'], $params['consumer_secret'], $access_token_used ? $params['access_token'] : null, $access_token_used ? $params['access_token_secret'] : null); - $timeline = $twitter->get('statuses/user_timeline', [ + $this->client = self::getHttpClient($params['consumer_key'], $params['consumer_secret'], $params['access_token'], $params['access_token_secret']); + + $this->items = $this->fetchTwitterTimeline('statuses/user_timeline', [ 'screen_name' => $params['username'], - 'include_rts' => 1, - 'count' => 50, - 'tweet_mode' => 'extended', ]); - if (isset($timeline->errors)) { - $errors = ''; - - foreach ($timeline->errors as $error) { - $errors .= $error->message . "\n"; - } - - throw new \Exception($errors); - } - - if (!is_array($timeline)) { - throw new \Exception('invalid twitter response'); - } - $this->items = $timeline; - $this->htmlUrl = 'https://twitter.com/' . urlencode($params['username']); $this->spoutTitle = "@{$params['username']}";