2022-04-04 22:13:05 +03:00
|
|
|
<?php
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
<a href="https://rss-bridge.github.io/rss-bridge/Bridge_Specific/TwitterV2.html">
|
|
|
|
Configuration Instructions</a>.';
|
|
|
|
const MAINTAINER = 'quickwick';
|
|
|
|
const CONFIGURATION = [
|
|
|
|
'twitterv2apitoken' => [
|
|
|
|
'required' => true,
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
2022-04-04 22:13:05 +03:00
|
|
|
];
|
|
|
|
const PARAMETERS = [
|
|
|
|
'global' => [
|
2022-04-06 21:56:56 +03:00
|
|
|
'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'
|
|
|
|
],
|
2022-04-04 22:13:05 +03:00
|
|
|
'maxresults' => [
|
|
|
|
'name' => 'Maximum results',
|
|
|
|
'required' => false,
|
|
|
|
'exampleValue' => '20',
|
|
|
|
'title' => 'Maximum number of tweets to retrieve (limit is 100)'
|
|
|
|
],
|
2022-04-07 10:00:28 +03:00
|
|
|
'imgonly' => [
|
|
|
|
'name' => 'Only media tweets',
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Activate to show only tweets with media (photo/video)'
|
|
|
|
],
|
2022-04-04 22:13:05 +03:00
|
|
|
'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',
|
2022-04-06 21:56:56 +03:00
|
|
|
'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)'
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
2022-04-04 22:13:05 +03:00
|
|
|
],
|
|
|
|
'By username' => [
|
|
|
|
'u' => [
|
|
|
|
'name' => 'username',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'sebsauvage',
|
|
|
|
'title' => 'Insert a user name'
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
2022-04-04 22:13:05 +03:00
|
|
|
],
|
|
|
|
'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`.
|
|
|
|
|
|
|
|
* To search for multiple words (contains any of these words), put "OR" between them.
|
|
|
|
|
|
|
|
Example: `rss-bridge OR rssbridge`.
|
|
|
|
|
|
|
|
* To search for an exact phrase (including whitespace), put double-quotes around them.
|
|
|
|
|
|
|
|
Example: `"rss-bridge release"`
|
|
|
|
|
|
|
|
* If you want to search for anything **but** a specific word, put a hyphen before it.
|
|
|
|
|
|
|
|
Example: `rss-bridge -release` (ignores "release")
|
|
|
|
|
|
|
|
* Of course, this also works for hashtags.
|
|
|
|
|
|
|
|
Example: `#rss-bridge OR #rssbridge`
|
|
|
|
|
|
|
|
* And you can combine them in any shape or form you like.
|
|
|
|
|
|
|
|
Example: `#rss-bridge OR #rssbridge -release`
|
|
|
|
EOD
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
2022-04-04 22:13:05 +03:00
|
|
|
],
|
|
|
|
'By list ID' => [
|
|
|
|
'listid' => [
|
|
|
|
'name' => 'List ID',
|
|
|
|
'exampleValue' => '31748',
|
|
|
|
'required' => true,
|
2022-04-06 21:56:56 +03:00
|
|
|
'title' => 'Enter a list id'
|
2022-07-01 16:10:30 +03:00
|
|
|
]
|
|
|
|
]
|
2022-04-04 22:13:05 +03:00
|
|
|
];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// $Item variable needs to be accessible from multiple functions without passing
|
|
|
|
private $item = [];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
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);
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
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 = [];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
$hideProfilePic = $this->getInput('nopic');
|
|
|
|
$hideImages = $this->getInput('noimg');
|
|
|
|
$hideReplies = $this->getInput('norep');
|
|
|
|
$hideRetweets = $this->getInput('noretweet');
|
|
|
|
$hidePinned = $this->getInput('nopinned');
|
2022-04-06 21:56:56 +03:00
|
|
|
$tweetFilter = $this->getInput('filter');
|
2022-04-04 22:13:05 +03:00
|
|
|
$maxResults = $this->getInput('maxresults');
|
|
|
|
if ($maxResults > 100) {
|
|
|
|
$maxResults = 100;
|
|
|
|
}
|
2022-04-06 21:56:56 +03:00
|
|
|
$idAsTitle = $this->getInput('idastitle');
|
2022-04-07 10:00:28 +03:00
|
|
|
$onlyMediaTweets = $this->getInput('imgonly');
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Read API token from config.ini.php, put into Header
|
2022-05-10 10:41:12 +03:00
|
|
|
$apiToken = $this->getOption('twitterv2apitoken');
|
|
|
|
$authHeaders = [
|
|
|
|
'authorization: Bearer ' . $apiToken,
|
2022-04-04 22:13:05 +03:00
|
|
|
];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// 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/'
|
2022-05-10 10:41:12 +03:00
|
|
|
. $this->getInput('u'), $authHeaders, $params);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
if (isset($user->errors)) {
|
|
|
|
Debug::log('User JSON: ' . json_encode($user));
|
|
|
|
returnServerError('Requested username can\'t be found.');
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// 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'
|
|
|
|
];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// 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';
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Get the tweets
|
|
|
|
$data = $this->makeApiCall('/users/' . $user->data->id
|
2022-05-10 10:41:12 +03:00
|
|
|
. '/tweets', $authHeaders, $params);
|
2022-04-04 22:13:05 +03:00
|
|
|
break;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
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'
|
|
|
|
];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-06 21:56:56 +03:00
|
|
|
// 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';
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
$data = $this->makeApiCall('/tweets/search/recent', $authHeaders, $params);
|
2022-04-04 22:13:05 +03:00
|
|
|
break;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
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'
|
|
|
|
];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
$data = $this->makeApiCall('/lists/' . $this->getInput('listid') .
|
2022-05-10 10:41:12 +03:00
|
|
|
'/tweets', $authHeaders, $params);
|
2022-04-04 22:13:05 +03:00
|
|
|
break;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
default:
|
|
|
|
returnServerError('Invalid query context !');
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
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.');
|
2022-06-24 19:29:35 +03:00
|
|
|
// fall-through
|
2022-04-04 22:13:05 +03:00
|
|
|
case 'By username':
|
|
|
|
returnServerError('Requested username cannnot be found.');
|
2022-06-24 19:29:35 +03:00
|
|
|
// fall-through
|
2022-04-04 22:13:05 +03:00
|
|
|
case 'By list ID':
|
|
|
|
returnServerError('Requested list cannnot be found');
|
2022-06-24 19:29:35 +03:00
|
|
|
// fall-through
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Extract Media data into array
|
|
|
|
isset($data->includes->media) ? $includesMedia = $data->includes->media : $includesMedia = null;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Extract additional Users data into array
|
|
|
|
isset($data->includes->users) ? $includesUsers = $data->includes->users : $includesUsers = null;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Extract additional Tweets data into array
|
|
|
|
isset($data->includes->tweets) ? $includesTweets = $data->includes->tweets : $includesTweets = null;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Extract main Tweets data into array
|
|
|
|
$tweets = $data->data;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// 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;
|
2022-05-10 10:41:12 +03:00
|
|
|
if (!$hideImages && isset($includesTweets)) {
|
2022-04-04 22:13:05 +03:00
|
|
|
// There has to be a better PHP way to extract the tweet Ids?
|
|
|
|
$includesTweetsIds = [];
|
|
|
|
foreach ($includesTweets as $includesTweet) {
|
|
|
|
$includesTweetsIds[] = $includesTweet->id;
|
|
|
|
}
|
2022-05-10 10:41:12 +03:00
|
|
|
Debug::log('includesTweetsIds: ' . join(',', $includesTweetsIds));
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// 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'
|
|
|
|
];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Get the retweeted tweets
|
2022-05-10 10:41:12 +03:00
|
|
|
$retweetedData = $this->makeApiCall('/tweets', $authHeaders, $params);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Extract retweets Media data into array
|
|
|
|
isset($retweetedData->includes->media) ? $retweetedMedia
|
|
|
|
= $retweetedData->includes->media : $retweetedMedia = null;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Extract retweets additional Users data into array
|
|
|
|
isset($retweetedData->includes->users) ? $retweetedUsers
|
|
|
|
= $retweetedData->includes->users : $retweetedUsers = null;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Create output array with all required elements for each tweet
|
|
|
|
foreach ($tweets as $tweet) {
|
|
|
|
//Debug::log('Tweet JSON: ' . json_encode($tweet));
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-07 10:00:28 +03:00
|
|
|
// Skip pinned tweet (if selected)
|
2022-04-04 22:13:05 +03:00
|
|
|
if ($hidePinned && $tweet->id === $pinnedTweetId) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Check if tweet is Retweet, Quote or Reply
|
2022-04-04 22:13:05 +03:00
|
|
|
$isRetweet = false;
|
2022-04-06 21:56:56 +03:00
|
|
|
$isReply = false;
|
2022-05-10 10:41:12 +03:00
|
|
|
$isQuote = false;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
if (isset($tweet->referenced_tweets)) {
|
2022-05-10 10:41:12 +03:00
|
|
|
switch ($tweet->referenced_tweets[0]->type) {
|
|
|
|
case 'retweeted':
|
|
|
|
$isRetweet = true;
|
|
|
|
break;
|
|
|
|
case 'quoted':
|
|
|
|
$isQuote = true;
|
|
|
|
break;
|
|
|
|
case 'replied_to':
|
|
|
|
$isReply = true;
|
|
|
|
break;
|
2022-04-06 21:56:56 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
|
2022-04-07 10:00:28 +03:00
|
|
|
// Skip replies and/or retweets (if selected). This check is primarily for lists
|
2022-04-06 21:56:56 +03:00
|
|
|
// These should already be pre-filtered for username and keyword queries
|
|
|
|
if (($hideRetweets && $isRetweet) || ($hideReplies && $isReply)) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-10 19:53:35 +03:00
|
|
|
$cleanedTweet = nl2br($tweet->text);
|
|
|
|
//Debug::log('cleanedTweet: ' . $cleanedTweet);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Perform optional keyword filtering (only keep tweet if keyword is found)
|
2022-04-06 21:56:56 +03:00
|
|
|
if (! empty($tweetFilter)) {
|
|
|
|
if (stripos($cleanedTweet, $this->getInput('filter')) === false) {
|
|
|
|
continue;
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Initialize empty array to hold feed item values
|
|
|
|
$this->item = [];
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
$quotedUser = $this->getTweetUser($quotedTweet, $retweetedUsers, $includesUsers);
|
|
|
|
}
|
2022-04-04 22:13:05 +03:00
|
|
|
if ($isRetweet || is_null($user)) {
|
2022-04-26 13:11:26 +03:00
|
|
|
Debug::log('Tweet is retweet, or $user is null');
|
2022-04-04 22:13:05 +03:00
|
|
|
// Replace tweet object with original retweeted object
|
|
|
|
if ($isRetweet) {
|
|
|
|
foreach ($includesTweets as $includesTweet) {
|
|
|
|
if ($includesTweet->id === $tweet->referenced_tweets[0]->id) {
|
|
|
|
$tweet = $includesTweet;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Skip self-Retweets (can cause duplicate entries in output)
|
|
|
|
if (isset($user) && $tweet->author_id === $user->data->id) {
|
|
|
|
continue;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Get user object for retweeted tweet
|
2022-05-10 10:41:12 +03:00
|
|
|
$originalUser = $this->getTweetUser($tweet, $retweetedUsers, $includesUsers);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
$this->item['username'] = $originalUser->username;
|
|
|
|
$this->item['fullname'] = $originalUser->name;
|
2022-04-04 22:13:05 +03:00
|
|
|
if (isset($originalUser->profile_image_url)) {
|
2022-05-10 10:41:12 +03:00
|
|
|
$this->item['avatar'] = $originalUser->profile_image_url;
|
2022-04-04 22:13:05 +03:00
|
|
|
} else {
|
2022-05-10 10:41:12 +03:00
|
|
|
$this->item['avatar'] = null;
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
|
|
|
} else {
|
2022-05-10 10:41:12 +03:00
|
|
|
$this->item['username'] = $user->data->username;
|
|
|
|
$this->item['fullname'] = $user->data->name;
|
|
|
|
$this->item['avatar'] = $user->data->profile_image_url;
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
2022-05-10 10:41:12 +03:00
|
|
|
$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']
|
2022-04-04 22:13:05 +03:00
|
|
|
. ' (@'
|
2022-05-10 10:41:12 +03:00
|
|
|
. $this->item['username'] . ')';
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// (Optional) Skip non-media tweet
|
2022-04-07 10:00:28 +03:00
|
|
|
// This check must wait until after retweets are identified
|
2022-05-10 10:41:12 +03:00
|
|
|
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;
|
2022-04-07 10:00:28 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Search for and replace URLs in Tweet text
|
2022-05-10 10:41:12 +03:00
|
|
|
$cleanedTweet = $this->replaceTweetURLs($tweet, $cleanedTweet);
|
|
|
|
if (isset($cleanedQuotedTweet)) {
|
|
|
|
Debug::log('Replacing URLs in Quoted Tweet text');
|
|
|
|
$cleanedQuotedTweet = $this->replaceTweetURLs($quotedTweet, $cleanedQuotedTweet);
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Generate Title text
|
2022-04-06 21:56:56 +03:00
|
|
|
if ($idAsTitle) {
|
|
|
|
$titleText = $tweet->id;
|
|
|
|
} else {
|
|
|
|
$titleText = strip_tags($cleanedTweet);
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-06 21:56:56 +03:00
|
|
|
if ($isRetweet && substr($titleText, 0, 4) === 'RT @') {
|
|
|
|
$titleText = substr_replace($titleText, ':', 2, 0);
|
|
|
|
} elseif ($isReply && !$idAsTitle) {
|
|
|
|
$titleText = 'R: ' . $titleText;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
$this->item['title'] = $titleText;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Generate Avatar HTML block
|
2022-04-04 22:13:05 +03:00
|
|
|
$picture_html = '';
|
2022-05-10 10:41:12 +03:00
|
|
|
if (!$hideProfilePic && isset($this->item['avatar'])) {
|
2022-04-04 22:13:05 +03:00
|
|
|
$picture_html = <<<EOD
|
2022-05-10 10:41:12 +03:00
|
|
|
<a href="https://twitter.com/{$this->item['username']}">
|
2022-04-04 22:13:05 +03:00
|
|
|
<img
|
2022-04-10 19:53:35 +03:00
|
|
|
style="margin-right: 10px; margin-bottom: 10px;"
|
2022-05-10 10:41:12 +03:00
|
|
|
alt="{$this->item['username']}"
|
|
|
|
src="{$this->item['avatar']}"
|
|
|
|
title="{$this->item['fullname']}" />
|
2022-04-04 22:13:05 +03:00
|
|
|
</a>
|
|
|
|
EOD;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Generate media HTML block
|
2022-04-04 22:13:05 +03:00
|
|
|
$media_html = '';
|
2022-05-10 10:41:12 +03:00
|
|
|
$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);
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
2022-05-10 10:41:12 +03:00
|
|
|
if (isset($quotedTweet->attachments->media_keys)) {
|
|
|
|
Debug::log('Generating HTML for quoted tweet media');
|
|
|
|
$quoted_media_html = $this->createTweetMediaHTML($quotedTweet, $includesMedia, $retweetedMedia);
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Generate the HTML for Item content
|
|
|
|
$this->item['content'] = <<<EOD
|
2022-04-10 19:53:35 +03:00
|
|
|
<div style="float: left;">
|
2022-04-04 22:13:05 +03:00
|
|
|
{$picture_html}
|
|
|
|
</div>
|
2022-04-10 19:53:35 +03:00
|
|
|
<div style="display: table;">
|
|
|
|
{$cleanedTweet}
|
2022-04-04 22:13:05 +03:00
|
|
|
</div>
|
2022-04-10 19:53:35 +03:00
|
|
|
<div style="display: block; margin-top: 16px;">
|
|
|
|
{$media_html}
|
2022-04-04 22:13:05 +03:00
|
|
|
EOD;
|
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// 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 ·
|
|
|
|
<a href="$quotedTweetURI">$quotedTweet->created_at</a></p>
|
|
|
|
$cleanedQuotedTweet
|
|
|
|
$quoted_media_html
|
|
|
|
</div>
|
|
|
|
QUOTE;
|
|
|
|
$this->item['content'] .= $quote_html;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
$this->item['content'] = htmlspecialchars_decode($this->item['content'], ENT_QUOTES);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
// Add current Item to Items array
|
|
|
|
$this->items[] = $this->item;
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
// Sort all tweets in array by date
|
|
|
|
usort($this->items, ['TwitterV2Bridge', 'compareTweetDate']);
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
private static function compareTweetDate($tweet1, $tweet2)
|
|
|
|
{
|
|
|
|
return (strtotime($tweet1['timestamp']) < strtotime($tweet2['timestamp']) ? 1 : -1);
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-04-04 22:13:05 +03:00
|
|
|
/**
|
|
|
|
* Tries to make an API call to Twitter.
|
|
|
|
* @param $api string API entry point
|
|
|
|
* @param $params array additional URI parmaeters
|
|
|
|
* @return object json data
|
|
|
|
*/
|
2022-05-10 10:41:12 +03:00
|
|
|
private function makeApiCall($api, $authHeaders, $params)
|
|
|
|
{
|
2022-04-04 22:13:05 +03:00
|
|
|
$uri = self::API_URI . $api . '?' . http_build_query($params);
|
2022-05-10 10:41:12 +03:00
|
|
|
$result = getContents($uri, $authHeaders, [], false);
|
2022-04-04 22:13:05 +03:00
|
|
|
$data = json_decode($result);
|
|
|
|
return $data;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
/**
|
|
|
|
* 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);
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
return $tweetText;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
/**
|
|
|
|
* 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;
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
2022-05-10 10:41:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
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;
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
}
|
2022-05-10 10:41:12 +03:00
|
|
|
}
|
|
|
|
return $matchedUser;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
/**
|
|
|
|
* 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
|
2022-07-01 16:10:30 +03:00
|
|
|
)
|
2022-05-10 10:41:12 +03:00
|
|
|
) {
|
|
|
|
Debug::log('Found media_key in $includesMedia');
|
|
|
|
$tweetMedia[] = $includesMedium;
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
2022-05-10 10:41:12 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// 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
|
2022-07-01 16:10:30 +03:00
|
|
|
)
|
2022-05-10 10:41:12 +03:00
|
|
|
) {
|
|
|
|
Debug::log('Found media_key in $retweetedMedia');
|
|
|
|
$tweetMedia[] = $retweetedMedium;
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
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;
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
$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;
|
|
|
|
|
|
|
|
$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;
|
|
|
|
|
|
|
|
$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);
|
|
|
|
}
|
|
|
|
}
|
2022-07-01 16:10:30 +03:00
|
|
|
|
2022-05-10 10:41:12 +03:00
|
|
|
return $media_html;
|
|
|
|
}
|
2022-04-04 22:13:05 +03:00
|
|
|
}
|