aboutsummaryrefslogtreecommitdiff
path: root/bridges/TwitterV2Bridge.php
diff options
context:
space:
mode:
Diffstat (limited to 'bridges/TwitterV2Bridge.php')
-rw-r--r--bridges/TwitterV2Bridge.php1260
1 files changed, 644 insertions, 616 deletions
diff --git a/bridges/TwitterV2Bridge.php b/bridges/TwitterV2Bridge.php
index 1636139d..cad88598 100644
--- a/bridges/TwitterV2Bridge.php
+++ b/bridges/TwitterV2Bridge.php
@@ -1,93 +1,95 @@
<?php
+
/**
* TwitterV2Bridge leverages Twitter API v2, and requires
* a unique API Bearer Token, which requires creation of
* a Twitter Dev account. Link to instructions in DESCRIPTION.
*/
-class TwitterV2Bridge extends BridgeAbstract {
- const NAME = 'Twitter V2 Bridge';
- const URI = 'https://twitter.com/';
- const API_URI = 'https://api.twitter.com/2';
- const DESCRIPTION = 'Returns tweets (using Twitter API v2). See the
+class TwitterV2Bridge extends BridgeAbstract
+{
+ const NAME = 'Twitter V2 Bridge';
+ const URI = 'https://twitter.com/';
+ const API_URI = 'https://api.twitter.com/2';
+ const DESCRIPTION = 'Returns tweets (using Twitter API v2). See the
<a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/TwitterV2.html">
Configuration Instructions</a>.';
- const MAINTAINER = 'quickwick';
- const CONFIGURATION = array(
- 'twitterv2apitoken' => array(
- 'required' => true,
- )
- );
- const PARAMETERS = array(
- 'global' => array(
- 'filter' => array(
- 'name' => 'Filter',
- 'exampleValue' => 'rss-bridge',
- 'required' => false,
- 'title' => 'Specify a single term to search for'
- ),
- 'norep' => array(
- 'name' => 'Without replies',
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude reply tweets'
- ),
- 'noretweet' => array(
- 'name' => 'Without retweets',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude retweets'
- ),
- 'nopinned' => array(
- 'name' => 'Without pinned tweet',
- 'required' => false,
- 'type' => 'checkbox',
- 'title' => 'Activate to exclude pinned tweets'
- ),
- 'maxresults' => array(
- 'name' => 'Maximum results',
- 'required' => false,
- 'exampleValue' => '20',
- 'title' => 'Maximum number of tweets to retrieve (limit is 100)'
- ),
- 'imgonly' => array(
- 'name' => 'Only media tweets',
- 'type' => 'checkbox',
- 'title' => 'Activate to show only tweets with media (photo/video)'
- ),
- 'nopic' => array(
- 'name' => 'Hide profile pictures',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide profile pictures in content'
- ),
- 'noimg' => array(
- 'name' => 'Hide images in tweets',
- 'type' => 'checkbox',
- 'title' => 'Activate to hide images in tweets'
- ),
- 'noimgscaling' => array(
- 'name' => 'Disable image scaling',
- 'type' => 'checkbox',
- 'title' => 'Activate to display original sized images (no thumbnails)'
- ),
- 'idastitle' => array(
- 'name' => 'Use tweet id as title',
- 'type' => 'checkbox',
- 'title' => 'Activate to use tweet id as title (instead of tweet text)'
- )
- ),
- 'By username' => array(
- 'u' => array(
- 'name' => 'username',
- 'required' => true,
- 'exampleValue' => 'sebsauvage',
- 'title' => 'Insert a user name'
- )
- ),
- 'By keyword or hashtag' => array(
- 'query' => array(
- 'name' => 'Keyword or #hashtag',
- 'required' => true,
- 'exampleValue' => 'rss-bridge OR #rss-bridge',
- 'title' => <<<EOD
+ const MAINTAINER = 'quickwick';
+ const CONFIGURATION = [
+ 'twitterv2apitoken' => [
+ 'required' => true,
+ ]
+ ];
+ const PARAMETERS = [
+ 'global' => [
+ 'filter' => [
+ 'name' => 'Filter',
+ 'exampleValue' => 'rss-bridge',
+ 'required' => false,
+ 'title' => 'Specify a single term to search for'
+ ],
+ 'norep' => [
+ 'name' => 'Without replies',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude reply tweets'
+ ],
+ 'noretweet' => [
+ 'name' => 'Without retweets',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude retweets'
+ ],
+ 'nopinned' => [
+ 'name' => 'Without pinned tweet',
+ 'required' => false,
+ 'type' => 'checkbox',
+ 'title' => 'Activate to exclude pinned tweets'
+ ],
+ 'maxresults' => [
+ 'name' => 'Maximum results',
+ 'required' => false,
+ 'exampleValue' => '20',
+ 'title' => 'Maximum number of tweets to retrieve (limit is 100)'
+ ],
+ 'imgonly' => [
+ 'name' => 'Only media tweets',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to show only tweets with media (photo/video)'
+ ],
+ 'nopic' => [
+ 'name' => 'Hide profile pictures',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide profile pictures in content'
+ ],
+ 'noimg' => [
+ 'name' => 'Hide images in tweets',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to hide images in tweets'
+ ],
+ 'noimgscaling' => [
+ 'name' => 'Disable image scaling',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to display original sized images (no thumbnails)'
+ ],
+ 'idastitle' => [
+ 'name' => 'Use tweet id as title',
+ 'type' => 'checkbox',
+ 'title' => 'Activate to use tweet id as title (instead of tweet text)'
+ ]
+ ],
+ 'By username' => [
+ 'u' => [
+ 'name' => 'username',
+ 'required' => true,
+ 'exampleValue' => 'sebsauvage',
+ 'title' => 'Insert a user name'
+ ]
+ ],
+ 'By keyword or hashtag' => [
+ 'query' => [
+ 'name' => 'Keyword or #hashtag',
+ 'required' => true,
+ 'exampleValue' => 'rss-bridge OR #rss-bridge',
+ 'title' => <<<EOD
* To search for multiple words (must contain all of these words), put a space between them.
Example: `rss-bridge release`.
@@ -112,353 +114,362 @@ Example: `#rss-bridge OR #rssbridge`
Example: `#rss-bridge OR #rssbridge -release`
EOD
- )
- ),
- 'By list ID' => array(
- 'listid' => array(
- 'name' => 'List ID',
- 'exampleValue' => '31748',
- 'required' => true,
- 'title' => 'Enter a list id'
- )
- )
- );
-
- // $Item variable needs to be accessible from multiple functions without passing
- private $item = array();
-
- public function getName() {
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- $specific = 'search ';
- $param = 'query';
- break;
- case 'By username':
- $specific = '@';
- $param = 'u';
- break;
- case 'By list ID':
- return 'Twitter List #' . $this->getInput('listid');
- default:
- return parent::getName();
- }
- return 'Twitter ' . $specific . $this->getInput($param);
- }
-
- public function collectData() {
- // $data will contain an array of all found tweets
- $data = null;
- // Contains user data (when in by username context)
- $user = null;
- // Array of all found tweets
- $tweets = array();
-
- $hideProfilePic = $this->getInput('nopic');
- $hideImages = $this->getInput('noimg');
- $hideReplies = $this->getInput('norep');
- $hideRetweets = $this->getInput('noretweet');
- $hidePinned = $this->getInput('nopinned');
- $tweetFilter = $this->getInput('filter');
- $maxResults = $this->getInput('maxresults');
- if ($maxResults > 100) {
- $maxResults = 100;
- }
- $idAsTitle = $this->getInput('idastitle');
- $onlyMediaTweets = $this->getInput('imgonly');
-
- // Read API token from config.ini.php, put into Header
- $apiToken = $this->getOption('twitterv2apitoken');
- $authHeaders = array(
- 'authorization: Bearer ' . $apiToken,
- );
-
- // Try to get all tweets
- switch($this->queriedContext) {
- case 'By username':
- //Get id from username
- $params = array(
- 'user.fields' => 'pinned_tweet_id,profile_image_url'
- );
- $user = $this->makeApiCall('/users/by/username/'
- . $this->getInput('u'), $authHeaders, $params);
-
- if(isset($user->errors)) {
- Debug::log('User JSON: ' . json_encode($user));
- returnServerError('Requested username can\'t be found.');
- }
-
- // Set default params
- $params = array(
- 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
- 'tweet.fields'
- => 'created_at,referenced_tweets,entities,attachments',
- 'user.fields' => 'pinned_tweet_id',
- 'expansions'
- => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url'
- );
-
- // Set params to filter out replies and/or retweets
- if($hideReplies && $hideRetweets) {
- $params['exclude'] = 'replies,retweets';
- } elseif($hideReplies) {
- $params['exclude'] = 'replies';
- } elseif($hideRetweets) {
- $params['exclude'] = 'retweets';
- }
-
- // Get the tweets
- $data = $this->makeApiCall('/users/' . $user->data->id
- . '/tweets', $authHeaders, $params);
- break;
-
- case 'By keyword or hashtag':
- $params = array(
- 'query' => $this->getInput('query'),
- 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
- 'tweet.fields'
- => 'created_at,referenced_tweets,entities,attachments',
- 'expansions'
- => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url'
- );
-
- // Set params to filter out replies and/or retweets
- if($hideReplies) {
- $params['query'] = $params['query'] . ' -is:reply';
- }
- if($hideRetweets) {
- $params['query'] = $params['query'] . ' -is:retweet';
- }
-
- $data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params);
- break;
-
- case 'By list ID':
- // Set default params
- $params = array(
- 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
- 'tweet.fields'
- => 'created_at,referenced_tweets,entities,attachments',
- 'expansions'
- => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url'
- );
-
- $data = $this->makeApiCall('/lists/' . $this->getInput('listid') .
- '/tweets', $authHeaders, $params);
- break;
-
- default:
- returnServerError('Invalid query context !');
- }
-
- if((isset($data->errors) && !isset($data->data)) ||
- (isset($data->meta) && $data->meta->result_count === 0)) {
- Debug::log('Data JSON: ' . json_encode($data));
- switch($this->queriedContext) {
- case 'By keyword or hashtag':
- returnServerError('No results for this query.');
- // fall-through
- case 'By username':
- returnServerError('Requested username cannnot be found.');
- // fall-through
- case 'By list ID':
- returnServerError('Requested list cannnot be found');
- // fall-through
- }
- }
-
- // figure out the Pinned Tweet Id
- if($hidePinned) {
- $pinnedTweetId = null;
- if(isset($user) && isset($user->data->pinned_tweet_id)) {
- $pinnedTweetId = $user->data->pinned_tweet_id;
- }
- }
-
- // Extract Media data into array
- isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null;
-
- // Extract additional Users data into array
- isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null;
-
- // Extract additional Tweets data into array
- isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null;
-
- // Extract main Tweets data into array
- $tweets = $data->data;
-
- // Make another API call to get user and media info for retweets
- // Is there some way to get this info included in original API call?
- $retweetedData = null;
- $retweetedMedia = null;
- $retweetedUsers = null;
- if(!$hideImages && isset($includesTweets)) {
- // There has to be a better PHP way to extract the tweet Ids?
- $includesTweetsIds = array();
- foreach($includesTweets as $includesTweet) {
- $includesTweetsIds[] = $includesTweet->id;
- }
- Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
-
- // Set default params for API query
- $params = array(
- 'ids' => join(',', $includesTweetsIds),
- 'tweet.fields' => 'entities,attachments',
- 'expansions' => 'author_id,attachments.media_keys',
- 'media.fields' => 'type,url,preview_image_url',
- 'user.fields' => 'id,profile_image_url'
- );
-
- // Get the retweeted tweets
- $retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params);
-
- // Extract retweets Media data into array
- isset($retweetedData->includes->media) ? $retweetedMedia
- = $retweetedData->includes->media : $retweetedMedia = null;
-
- // Extract retweets additional Users data into array
- isset($retweetedData->includes->users) ? $retweetedUsers
- = $retweetedData->includes->users : $retweetedUsers = null;
- }
-
- // Create output array with all required elements for each tweet
- foreach($tweets as $tweet) {
- //Debug::log('Tweet JSON: ' . json_encode($tweet));
-
- // Skip pinned tweet (if selected)
- if($hidePinned && $tweet->id === $pinnedTweetId) {
- continue;
- }
-
- // Check if tweet is Retweet, Quote or Reply
- $isRetweet = false;
- $isReply = false;
- $isQuote = false;
-
- if(isset($tweet->referenced_tweets)) {
- switch($tweet->referenced_tweets[0]->type) {
- case 'retweeted':
- $isRetweet = true; break;
- case 'quoted':
- $isQuote = true; break;
- case 'replied_to':
- $isReply = true; break;
- }
- }
-
- // Skip replies and/or retweets (if selected). This check is primarily for lists
- // These should already be pre-filtered for username and keyword queries
- if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) {
- continue;
- }
-
- $cleanedTweet = nl2br($tweet->text);
- //Debug::log('cleanedTweet: ' . $cleanedTweet);
-
- // Perform optional keyword filtering (only keep tweet if keyword is found)
- if (! empty($tweetFilter)) {
- if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
- continue;
- }
- }
-
- // Initialize empty array to hold feed item values
- $this->item = array();
-
- // Start getting and setting values needed for HTML output
- $quotedTweet = null;
- $cleanedQuotedTweet = null;
- $quotedUser = null;
- if ($isQuote) {
- Debug::log('Tweet is quote');
- foreach($includesTweets as $includesTweet) {
- if($includesTweet->id === $tweet->referenced_tweets[0]->id) {
- $quotedTweet = $includesTweet;
- $cleanedQuotedTweet = nl2br($quotedTweet->text);
- //Debug::log('Found quoted tweet');
- break;
- }
- }
-
- $quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers);
- }
- if($isRetweet || is_null($user)) {
- Debug::log('Tweet is retweet, or $user is null');
- // Replace tweet object with original retweeted object
- if($isRetweet) {
- foreach($includesTweets as $includesTweet) {
- if($includesTweet->id === $tweet->referenced_tweets[0]->id) {
- $tweet = $includesTweet;
- break;
- }
- }
- }
-
- // Skip self-Retweets (can cause duplicate entries in output)
- if(isset($user) && $tweet->author_id === $user->data->id) {
- continue;
- }
-
- // Get user object for retweeted tweet
- $originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers);
-
- $this->item['username'] = $originalUser->username;
- $this->item['fullname'] = $originalUser->name;
- if(isset($originalUser->profile_image_url)) {
- $this->item['avatar'] = $originalUser->profile_image_url;
- } else{
- $this->item['avatar'] = null;
- }
- } else{
- $this->item['username'] = $user->data->username;
- $this->item['fullname'] = $user->data->name;
- $this->item['avatar'] = $user->data->profile_image_url;
- }
- $this->item['id'] = $tweet->id;
- $this->item['timestamp'] = $tweet->created_at;
- $this->item['uri']
- = self::URI . $this->item['username'] . '/status/' . $this->item['id'];
- $this->item['author'] = ($isRetweet ? 'RT: ' : '' )
- . $this->item['fullname']
- . ' (@'
- . $this->item['username'] . ')';
-
- // (Optional) Skip non-media tweet
- // This check must wait until after retweets are identified
- if ($onlyMediaTweets && !isset($tweet->attachments->media_keys) &&
- (($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote)) {
- // There is no media in current tweet or quoted tweet, skip to next
- continue;
- }
-
- // Search for and replace URLs in Tweet text
- $cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet);
- if (isset($cleanedQuotedTweet)) {
- Debug::log('Replacing URLs in Quoted Tweet text');
- $cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet);
- }
-
- // Generate Title text
- if ($idAsTitle) {
- $titleText = $tweet->id;
- } else{
- $titleText = strip_tags($cleanedTweet);
- }
-
- if($isRetweet && substr($titleText, 0, 4) === 'RT @') {
- $titleText = substr_replace($titleText, ':', 2, 0 );
- } elseif ($isReply && !$idAsTitle) {
- $titleText = 'R: ' . $titleText;
- }
-
- $this->item['title'] = $titleText;
-
- // Generate Avatar HTML block
- $picture_html = '';
- if(!$hideProfilePic && isset($this->item['avatar'])) {
- $picture_html = <<<EOD
+ ]
+ ],
+ 'By list ID' => [
+ 'listid' => [
+ 'name' => 'List ID',
+ 'exampleValue' => '31748',
+ 'required' => true,
+ 'title' => 'Enter a list id'
+ ]
+ ]
+ ];
+
+ // $Item variable needs to be accessible from multiple functions without passing
+ private $item = [];
+
+ public function getName()
+ {
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ $specific = 'search ';
+ $param = 'query';
+ break;
+ case 'By username':
+ $specific = '@';
+ $param = 'u';
+ break;
+ case 'By list ID':
+ return 'Twitter List #' . $this->getInput('listid');
+ default:
+ return parent::getName();
+ }
+ return 'Twitter ' . $specific . $this->getInput($param);
+ }
+
+ public function collectData()
+ {
+ // $data will contain an array of all found tweets
+ $data = null;
+ // Contains user data (when in by username context)
+ $user = null;
+ // Array of all found tweets
+ $tweets = [];
+
+ $hideProfilePic = $this->getInput('nopic');
+ $hideImages = $this->getInput('noimg');
+ $hideReplies = $this->getInput('norep');
+ $hideRetweets = $this->getInput('noretweet');
+ $hidePinned = $this->getInput('nopinned');
+ $tweetFilter = $this->getInput('filter');
+ $maxResults = $this->getInput('maxresults');
+ if ($maxResults > 100) {
+ $maxResults = 100;
+ }
+ $idAsTitle = $this->getInput('idastitle');
+ $onlyMediaTweets = $this->getInput('imgonly');
+
+ // Read API token from config.ini.php, put into Header
+ $apiToken = $this->getOption('twitterv2apitoken');
+ $authHeaders = [
+ 'authorization: Bearer ' . $apiToken,
+ ];
+
+ // Try to get all tweets
+ switch ($this->queriedContext) {
+ case 'By username':
+ //Get id from username
+ $params = [
+ 'user.fields' => 'pinned_tweet_id,profile_image_url'
+ ];
+ $user = $this->makeApiCall('/users/by/username/'
+ . $this->getInput('u'), $authHeaders, $params);
+
+ if (isset($user->errors)) {
+ Debug::log('User JSON: ' . json_encode($user));
+ returnServerError('Requested username can\'t be found.');
+ }
+
+ // Set default params
+ $params = [
+ 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
+ 'tweet.fields'
+ => 'created_at,referenced_tweets,entities,attachments',
+ 'user.fields' => 'pinned_tweet_id',
+ 'expansions'
+ => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url'
+ ];
+
+ // Set params to filter out replies and/or retweets
+ if ($hideReplies && $hideRetweets) {
+ $params['exclude'] = 'replies,retweets';
+ } elseif ($hideReplies) {
+ $params['exclude'] = 'replies';
+ } elseif ($hideRetweets) {
+ $params['exclude'] = 'retweets';
+ }
+
+ // Get the tweets
+ $data = $this->makeApiCall('/users/' . $user->data->id
+ . '/tweets', $authHeaders, $params);
+ break;
+
+ case 'By keyword or hashtag':
+ $params = [
+ 'query' => $this->getInput('query'),
+ 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
+ 'tweet.fields'
+ => 'created_at,referenced_tweets,entities,attachments',
+ 'expansions'
+ => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url'
+ ];
+
+ // Set params to filter out replies and/or retweets
+ if ($hideReplies) {
+ $params['query'] = $params['query'] . ' -is:reply';
+ }
+ if ($hideRetweets) {
+ $params['query'] = $params['query'] . ' -is:retweet';
+ }
+
+ $data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params);
+ break;
+
+ case 'By list ID':
+ // Set default params
+ $params = [
+ 'max_results' => (empty($maxResults) ? '10' : $maxResults ),
+ 'tweet.fields'
+ => 'created_at,referenced_tweets,entities,attachments',
+ 'expansions'
+ => 'referenced_tweets.id.author_id,entities.mentions.username,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url'
+ ];
+
+ $data = $this->makeApiCall('/lists/' . $this->getInput('listid') .
+ '/tweets', $authHeaders, $params);
+ break;
+
+ default:
+ returnServerError('Invalid query context !');
+ }
+
+ if (
+ (isset($data->errors) && !isset($data->data)) ||
+ (isset($data->meta) && $data->meta->result_count === 0)
+ ) {
+ Debug::log('Data JSON: ' . json_encode($data));
+ switch ($this->queriedContext) {
+ case 'By keyword or hashtag':
+ returnServerError('No results for this query.');
+ // fall-through
+ case 'By username':
+ returnServerError('Requested username cannnot be found.');
+ // fall-through
+ case 'By list ID':
+ returnServerError('Requested list cannnot be found');
+ // fall-through
+ }
+ }
+
+ // figure out the Pinned Tweet Id
+ if ($hidePinned) {
+ $pinnedTweetId = null;
+ if (isset($user) && isset($user->data->pinned_tweet_id)) {
+ $pinnedTweetId = $user->data->pinned_tweet_id;
+ }
+ }
+
+ // Extract Media data into array
+ isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null;
+
+ // Extract additional Users data into array
+ isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null;
+
+ // Extract additional Tweets data into array
+ isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null;
+
+ // Extract main Tweets data into array
+ $tweets = $data->data;
+
+ // Make another API call to get user and media info for retweets
+ // Is there some way to get this info included in original API call?
+ $retweetedData = null;
+ $retweetedMedia = null;
+ $retweetedUsers = null;
+ if (!$hideImages && isset($includesTweets)) {
+ // There has to be a better PHP way to extract the tweet Ids?
+ $includesTweetsIds = [];
+ foreach ($includesTweets as $includesTweet) {
+ $includesTweetsIds[] = $includesTweet->id;
+ }
+ Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
+
+ // Set default params for API query
+ $params = [
+ 'ids' => join(',', $includesTweetsIds),
+ 'tweet.fields' => 'entities,attachments',
+ 'expansions' => 'author_id,attachments.media_keys',
+ 'media.fields' => 'type,url,preview_image_url',
+ 'user.fields' => 'id,profile_image_url'
+ ];
+
+ // Get the retweeted tweets
+ $retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params);
+
+ // Extract retweets Media data into array
+ isset($retweetedData->includes->media) ? $retweetedMedia
+ = $retweetedData->includes->media : $retweetedMedia = null;
+
+ // Extract retweets additional Users data into array
+ isset($retweetedData->includes->users) ? $retweetedUsers
+ = $retweetedData->includes->users : $retweetedUsers = null;
+ }
+
+ // Create output array with all required elements for each tweet
+ foreach ($tweets as $tweet) {
+ //Debug::log('Tweet JSON: ' . json_encode($tweet));
+
+ // Skip pinned tweet (if selected)
+ if ($hidePinned && $tweet->id === $pinnedTweetId) {
+ continue;
+ }
+
+ // Check if tweet is Retweet, Quote or Reply
+ $isRetweet = false;
+ $isReply = false;
+ $isQuote = false;
+
+ if (isset($tweet->referenced_tweets)) {
+ switch ($tweet->referenced_tweets[0]->type) {
+ case 'retweeted':
+ $isRetweet = true;
+ break;
+ case 'quoted':
+ $isQuote = true;
+ break;
+ case 'replied_to':
+ $isReply = true;
+ break;
+ }
+ }
+
+ // Skip replies and/or retweets (if selected). This check is primarily for lists
+ // These should already be pre-filtered for username and keyword queries
+ if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) {
+ continue;
+ }
+
+ $cleanedTweet = nl2br($tweet->text);
+ //Debug::log('cleanedTweet: ' . $cleanedTweet);
+
+ // Perform optional keyword filtering (only keep tweet if keyword is found)
+ if (! empty($tweetFilter)) {
+ if (stripos($cleanedTweet, $this->getInput('filter')) === false) {
+ continue;
+ }
+ }
+
+ // Initialize empty array to hold feed item values
+ $this->item = [];
+
+ // Start getting and setting values needed for HTML output
+ $quotedTweet = null;
+ $cleanedQuotedTweet = null;
+ $quotedUser = null;
+ if ($isQuote) {
+ Debug::log('Tweet is quote');
+ foreach ($includesTweets as $includesTweet) {
+ if ($includesTweet->id === $tweet->referenced_tweets[0]->id) {
+ $quotedTweet = $includesTweet;
+ $cleanedQuotedTweet = nl2br($quotedTweet->text);
+ //Debug::log('Found quoted tweet');
+ break;
+ }
+ }
+
+ $quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers);
+ }
+ if ($isRetweet || is_null($user)) {
+ Debug::log('Tweet is retweet, or $user is null');
+ // Replace tweet object with original retweeted object
+ if ($isRetweet) {
+ foreach ($includesTweets as $includesTweet) {
+ if ($includesTweet->id === $tweet->referenced_tweets[0]->id) {
+ $tweet = $includesTweet;
+ break;
+ }
+ }
+ }
+
+ // Skip self-Retweets (can cause duplicate entries in output)
+ if (isset($user) && $tweet->author_id === $user->data->id) {
+ continue;
+ }
+
+ // Get user object for retweeted tweet
+ $originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers);
+
+ $this->item['username'] = $originalUser->username;
+ $this->item['fullname'] = $originalUser->name;
+ if (isset($originalUser->profile_image_url)) {
+ $this->item['avatar'] = $originalUser->profile_image_url;
+ } else {
+ $this->item['avatar'] = null;
+ }
+ } else {
+ $this->item['username'] = $user->data->username;
+ $this->item['fullname'] = $user->data->name;
+ $this->item['avatar'] = $user->data->profile_image_url;
+ }
+ $this->item['id'] = $tweet->id;
+ $this->item['timestamp'] = $tweet->created_at;
+ $this->item['uri']
+ = self::URI . $this->item['username'] . '/status/' . $this->item['id'];
+ $this->item['author'] = ($isRetweet ? 'RT: ' : '' )
+ . $this->item['fullname']
+ . ' (@'
+ . $this->item['username'] . ')';
+
+ // (Optional) Skip non-media tweet
+ // This check must wait until after retweets are identified
+ if (
+ $onlyMediaTweets && !isset($tweet->attachments->media_keys) &&
+ (($isQuote && !isset($quotedTweet->attachments->media_keys)) || !$isQuote)
+ ) {
+ // There is no media in current tweet or quoted tweet, skip to next
+ continue;
+ }
+
+ // Search for and replace URLs in Tweet text
+ $cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet);
+ if (isset($cleanedQuotedTweet)) {
+ Debug::log('Replacing URLs in Quoted Tweet text');
+ $cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet);
+ }
+
+ // Generate Title text
+ if ($idAsTitle) {
+ $titleText = $tweet->id;
+ } else {
+ $titleText = strip_tags($cleanedTweet);
+ }
+
+ if ($isRetweet && substr($titleText, 0, 4) === 'RT @') {
+ $titleText = substr_replace($titleText, ':', 2, 0);
+ } elseif ($isReply && !$idAsTitle) {
+ $titleText = 'R: ' . $titleText;
+ }
+
+ $this->item['title'] = $titleText;
+
+ // Generate Avatar HTML block
+ $picture_html = '';
+ if (!$hideProfilePic && isset($this->item['avatar'])) {
+ $picture_html = <<<EOD
<a href="https://twitter.com/{$this->item['username']}">
<img
style="margin-right: 10px; margin-bottom: 10px;"
@@ -467,24 +478,24 @@ EOD
title="{$this->item['fullname']}" />
</a>
EOD;
- }
-
- // Generate media HTML block
- $media_html = '';
- $quoted_media_html = '';
- if(!$hideImages) {
- if (isset($tweet->attachments->media_keys)) {
- Debug::log('Generating HTML for tweet media');
- $media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia);
- }
- if (isset($quotedTweet->attachments->media_keys)) {
- Debug::log('Generating HTML for quoted tweet media');
- $quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia);
- }
- }
-
- // Generate the HTML for Item content
- $this->item['content'] = <<<EOD
+ }
+
+ // Generate media HTML block
+ $media_html = '';
+ $quoted_media_html = '';
+ if (!$hideImages) {
+ if (isset($tweet->attachments->media_keys)) {
+ Debug::log('Generating HTML for tweet media');
+ $media_html = $this->createTweetMediaHTML($tweet, $includesMedia, $retweetedMedia);
+ }
+ if (isset($quotedTweet->attachments->media_keys)) {
+ Debug::log('Generating HTML for quoted tweet media');
+ $quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia);
+ }
+ }
+
+ // Generate the HTML for Item content
+ $this->item['content'] = <<<EOD
<div style="float: left;">
{$picture_html}
</div>
@@ -495,10 +506,10 @@ EOD;
{$media_html}
EOD;
- // Add Quoted Tweet HTML, if relevant
- if (isset($quotedTweet)) {
- $quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id;
- $quote_html = <<<QUOTE
+ // Add Quoted Tweet HTML, if relevant
+ if (isset($quotedTweet)) {
+ $quotedTweetURI = self::URI . $quotedUser->username . '/status/' . $quotedTweet->id;
+ $quote_html = <<<QUOTE
<div style="display: table; border-style: solid; border-width: 1px;
border-radius: 5px; padding: 5px;">
<p><b>$quotedUser->name</b> @$quotedUser->username ยท
@@ -507,183 +518,200 @@ EOD;
$quoted_media_html
</div>
QUOTE;
- $this->item['content'] .= $quote_html;
- }
-
- $this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES);
-
- // Add current Item to Items array
- $this->items[] = $this->item;
- }
-
- // Sort all tweets in array by date
- usort($this->items, array('TwitterV2Bridge', 'compareTweetDate'));
- }
-
- private static function compareTweetDate($tweet1, $tweet2) {
- return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1);
- }
-
- /**
- * Tries to make an API call to Twitter.
- * @param $api string API entry point
- * @param $params array additional URI parmaeters
- * @return object json data
- */
- private function makeApiCall($api, $authHeaders, $params) {
- $uri = self::API_URI . $api . '?' . http_build_query($params);
- $result = getContents($uri, $authHeaders, array(), false);
- $data = json_decode($result);
- return $data;
- }
-
- /**
- * Change format of URLs in tweet text
- * @param $tweetObject object current Tweet JSON
- * @param $tweetText string current Tweet text
- * @return string modified tweet text
- */
- private function replaceTweetURLs($tweetObject, $tweetText) {
- $foundUrls = false;
- // Rewrite URL links, based on URL list in tweet object
- if(isset($tweetObject->entities->urls)) {
- foreach($tweetObject->entities->urls as $url) {
- $tweetText = str_replace($url->url,
- '<a href="' . $url->expanded_url
- . '">' . $url->display_url . '</a>',
- $tweetText);
- }
- $foundUrls = true;
- }
- // Regex fallback for rewriting URL links. Should never trigger?
- if($foundUrls === false) {
- $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
- if(preg_match($reg_ex, $tweetText, $url)) {
- $tweetText = preg_replace($reg_ex,
- "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
- $tweetText);
- }
- }
- // Fix back-to-back URLs by adding a <br>
- $reg_ex = '/\/a>\s*<a/';
- $tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText);
-
- return $tweetText;
- }
-
- /**
- * Find User object for Retweeted/Quoted tweet
- * @param $tweetObject object current Tweet JSON
- * @param $retweetedUsers
- * @param $includesUsers
- * @return object found User
- */
- private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers) {
- $originalUser = new stdClass(); // make the linters stop complaining
- if(isset($retweetedUsers)) {
- Debug::log('Searching for tweet author_id in $retweetedUsers');
- foreach($retweetedUsers as $retweetedUser) {
- if($retweetedUser->id === $tweetObject->author_id) {
- $matchedUser = $retweetedUser;
- Debug::log('Found author_id match in $retweetedUsers');
- break;
- }
- }
- }
- if(!isset($matchedUser->username) && isset($includesUsers)) {
- Debug::log('Searching for tweet author_id in $includesUsers');
- foreach($includesUsers as $includesUser) {
- if($includesUser->id === $tweetObject->author_id) {
- $matchedUser = $includesUser;
- Debug::log('Found author_id match in $includesUsers');
- break;
- }
- }
- }
- return $matchedUser;
- }
-
- /**
- * Generates HTML for embedded media
- * @param $tweetObject object current Tweet JSON
- * @param $includesMedia
- * @param $retweetedMedia
- * @return string modified tweet text
- */
- private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia){
- $media_html = '';
- // Match media_keys in tweet to media list from, put matches into new array
- $tweetMedia = array();
- // Start by checking the original list of tweet Media includes
- if(isset($includesMedia)) {
- Debug::log('Searching for media_key in $includesMedia');
- foreach($includesMedia as $includesMedium) {
- if(in_array ($includesMedium->media_key,
- $tweetObject->attachments->media_keys)) {
- Debug::log('Found media_key in $includesMedia');
- $tweetMedia[] = $includesMedium;
- }
- }
- }
- // If no matches found, check the retweet Media includes
- if(empty($tweetMedia) && isset($retweetedMedia)) {
- Debug::log('Searching for media_key in $retweetedMedia');
- foreach($retweetedMedia as $retweetedMedium) {
- if(in_array ($retweetedMedium->media_key,
- $tweetObject->attachments->media_keys)) {
- Debug::log('Found media_key in $retweetedMedia');
- $tweetMedia[] = $retweetedMedium;
- }
- }
- }
-
- foreach($tweetMedia as $media) {
- switch($media->type) {
- case 'photo':
- if ($this->getInput('noimgscaling')) {
- $image = $media->url;
- $display_image = $media->url;
- } else{
- $image = $media->url . '?name=orig';
- $display_image = $media->url;
- }
- // add enclosures
- $this->item['enclosures'][] = $image;
-
- $media_html .= <<<EOD
+ $this->item['content'] .= $quote_html;
+ }
+
+ $this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES);
+
+ // Add current Item to Items array
+ $this->items[] = $this->item;
+ }
+
+ // Sort all tweets in array by date
+ usort($this->items, ['TwitterV2Bridge', 'compareTweetDate']);
+ }
+
+ private static function compareTweetDate($tweet1, $tweet2)
+ {
+ return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1);
+ }
+
+ /**
+ * Tries to make an API call to Twitter.
+ * @param $api string API entry point
+ * @param $params array additional URI parmaeters
+ * @return object json data
+ */
+ private function makeApiCall($api, $authHeaders, $params)
+ {
+ $uri = self::API_URI . $api . '?' . http_build_query($params);
+ $result = getContents($uri, $authHeaders, [], false);
+ $data = json_decode($result);
+ return $data;
+ }
+
+ /**
+ * Change format of URLs in tweet text
+ * @param $tweetObject object current Tweet JSON
+ * @param $tweetText string current Tweet text
+ * @return string modified tweet text
+ */
+ private function replaceTweetURLs($tweetObject, $tweetText)
+ {
+ $foundUrls = false;
+ // Rewrite URL links, based on URL list in tweet object
+ if (isset($tweetObject->entities->urls)) {
+ foreach ($tweetObject->entities->urls as $url) {
+ $tweetText = str_replace(
+ $url->url,
+ '<a href="' . $url->expanded_url
+ . '">' . $url->display_url . '</a>',
+ $tweetText
+ );
+ }
+ $foundUrls = true;
+ }
+ // Regex fallback for rewriting URL links. Should never trigger?
+ if ($foundUrls === false) {
+ $reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
+ if (preg_match($reg_ex, $tweetText, $url)) {
+ $tweetText = preg_replace(
+ $reg_ex,
+ "<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
+ $tweetText
+ );
+ }
+ }
+ // Fix back-to-back URLs by adding a <br>
+ $reg_ex = '/\/a>\s*<a/';
+ $tweetText = preg_replace($reg_ex, '/a><br><a', $tweetText);
+
+ return $tweetText;
+ }
+
+ /**
+ * Find User object for Retweeted/Quoted tweet
+ * @param $tweetObject object current Tweet JSON
+ * @param $retweetedUsers
+ * @param $includesUsers
+ * @return object found User
+ */
+ private function getTweetUser($tweetObject, $retweetedUsers, $includesUsers)
+ {
+ $originalUser = new stdClass(); // make the linters stop complaining
+ if (isset($retweetedUsers)) {
+ Debug::log('Searching for tweet author_id in $retweetedUsers');
+ foreach ($retweetedUsers as $retweetedUser) {
+ if ($retweetedUser->id === $tweetObject->author_id) {
+ $matchedUser = $retweetedUser;
+ Debug::log('Found author_id match in $retweetedUsers');
+ break;
+ }
+ }
+ }
+ if (!isset($matchedUser->username) && isset($includesUsers)) {
+ Debug::log('Searching for tweet author_id in $includesUsers');
+ foreach ($includesUsers as $includesUser) {
+ if ($includesUser->id === $tweetObject->author_id) {
+ $matchedUser = $includesUser;
+ Debug::log('Found author_id match in $includesUsers');
+ break;
+ }
+ }
+ }
+ return $matchedUser;
+ }
+
+ /**
+ * Generates HTML for embedded media
+ * @param $tweetObject object current Tweet JSON
+ * @param $includesMedia
+ * @param $retweetedMedia
+ * @return string modified tweet text
+ */
+ private function createTweetMediaHTML($tweetObject, $includesMedia, $retweetedMedia)
+ {
+ $media_html = '';
+ // Match media_keys in tweet to media list from, put matches into new array
+ $tweetMedia = [];
+ // Start by checking the original list of tweet Media includes
+ if (isset($includesMedia)) {
+ Debug::log('Searching for media_key in $includesMedia');
+ foreach ($includesMedia as $includesMedium) {
+ if (
+ in_array(
+ $includesMedium->media_key,
+ $tweetObject->attachments->media_keys
+ )
+ ) {
+ Debug::log('Found media_key in $includesMedia');
+ $tweetMedia[] = $includesMedium;
+ }
+ }
+ }
+ // If no matches found, check the retweet Media includes
+ if (empty($tweetMedia) && isset($retweetedMedia)) {
+ Debug::log('Searching for media_key in $retweetedMedia');
+ foreach ($retweetedMedia as $retweetedMedium) {
+ if (
+ in_array(
+ $retweetedMedium->media_key,
+ $tweetObject->attachments->media_keys
+ )
+ ) {
+ Debug::log('Found media_key in $retweetedMedia');
+ $tweetMedia[] = $retweetedMedium;
+ }
+ }
+ }
+
+ foreach ($tweetMedia as $media) {
+ switch ($media->type) {
+ case 'photo':
+ if ($this->getInput('noimgscaling')) {
+ $image = $media->url;
+ $display_image = $media->url;
+ } else {
+ $image = $media->url . '?name=orig';
+ $display_image = $media->url;
+ }
+ // add enclosures
+ $this->item['enclosures'][] = $image;
+
+ $media_html .= <<<EOD
<a href="{$image}">
<img
referrerpolicy="no-referrer"
src="{$display_image}" />
</a>
EOD;
- break;
- case 'video':
- // To Do: Is there a way to easily match this
- // to a direct Video URL?
- $display_image = $media->preview_image_url;
+ break;
+ case 'video':
+ // To Do: Is there a way to easily match this
+ // to a direct Video URL?
+ $display_image = $media->preview_image_url;
- $media_html .= <<<EOD
+ $media_html .= <<<EOD
<p>Video:</p><a href="{$this->item['uri']}">
<img referrerpolicy="no-referrer" src="{$display_image}" /></a>
EOD;
- break;
- case 'animated_gif':
- // To Do: Is there a way to easily match this to a
- // direct animated Gif URL?
- $display_image = $media->preview_image_url;
+ break;
+ case 'animated_gif':
+ // To Do: Is there a way to easily match this to a
+ // direct animated Gif URL?
+ $display_image = $media->preview_image_url;
- $media_html .= <<<EOD
+ $media_html .= <<<EOD
<p>Animated Gif:</p><a href="{$this->item['uri']}">
<img referrerpolicy="no-referrer" src="{$display_image}" /></a>
EOD;
- break;
- default:
- Debug::log('Missing support for media type: '
- . $media->type);
- }
- }
-
- return $media_html;
- }
+ break;
+ default:
+ Debug::log('Missing support for media type: '
+ . $media->type);
+ }
+ }
+
+ return $media_html;
+ }
}