2014-05-26 02:30:46 +04:00
|
|
|
<?php
|
2017-02-11 18:16:56 +03:00
|
|
|
class TwitterBridge extends BridgeAbstract {
|
|
|
|
const NAME = 'Twitter Bridge';
|
|
|
|
const URI = 'https://twitter.com/';
|
2020-06-05 11:17:53 +03:00
|
|
|
const API_URI = 'https://api.twitter.com';
|
2020-06-08 12:18:24 +03:00
|
|
|
const GUEST_TOKEN_USES = 100;
|
2020-06-23 16:14:50 +03:00
|
|
|
const GUEST_TOKEN_EXPIRY = 300; // 5min
|
2017-02-11 18:16:56 +03:00
|
|
|
const CACHE_TIMEOUT = 300; // 5min
|
|
|
|
const DESCRIPTION = 'returns tweets';
|
2016-12-17 18:43:47 +03:00
|
|
|
const MAINTAINER = 'pmaziere';
|
2017-02-11 18:16:56 +03:00
|
|
|
const PARAMETERS = array(
|
|
|
|
'global' => array(
|
|
|
|
'nopic' => array(
|
|
|
|
'name' => 'Hide profile pictures',
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Activate to hide profile pictures in content'
|
2017-04-22 16:36:25 +03:00
|
|
|
),
|
|
|
|
'noimg' => array(
|
|
|
|
'name' => 'Hide images in tweets',
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Activate to hide images in tweets'
|
2018-12-12 18:56:36 +03:00
|
|
|
),
|
|
|
|
'noimgscaling' => array(
|
|
|
|
'name' => 'Disable image scaling',
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Activate to disable image scaling in tweets (keeps original image)'
|
2017-02-11 18:16:56 +03:00
|
|
|
)
|
|
|
|
),
|
|
|
|
'By keyword or hashtag' => array(
|
|
|
|
'q' => array(
|
|
|
|
'name' => 'Keyword or #hashtag',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'rss-bridge, #rss-bridge',
|
2019-06-11 22:51:10 +03:00
|
|
|
'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
|
2017-02-11 18:16:56 +03:00
|
|
|
)
|
|
|
|
),
|
|
|
|
'By username' => array(
|
|
|
|
'u' => array(
|
|
|
|
'name' => 'username',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'sebsauvage',
|
|
|
|
'title' => 'Insert a user name'
|
|
|
|
),
|
|
|
|
'norep' => array(
|
|
|
|
'name' => 'Without replies',
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Only return initial tweets'
|
2017-03-17 20:41:35 +03:00
|
|
|
),
|
|
|
|
'noretweet' => array(
|
|
|
|
'name' => 'Without retweets',
|
|
|
|
'required' => false,
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Hide retweets'
|
2021-01-10 11:50:06 +03:00
|
|
|
),
|
|
|
|
'nopinned' => array(
|
|
|
|
'name' => 'Without pinned tweet',
|
|
|
|
'required' => false,
|
|
|
|
'type' => 'checkbox',
|
|
|
|
'title' => 'Hide pinned tweet'
|
2017-02-11 18:16:56 +03:00
|
|
|
)
|
2017-09-24 17:59:45 +03:00
|
|
|
),
|
|
|
|
'By list' => array(
|
|
|
|
'user' => array(
|
|
|
|
'name' => 'User',
|
|
|
|
'required' => true,
|
|
|
|
'exampleValue' => 'sebsauvage',
|
|
|
|
'title' => 'Insert a user name'
|
|
|
|
),
|
|
|
|
'list' => array(
|
|
|
|
'name' => 'List',
|
|
|
|
'required' => true,
|
|
|
|
'title' => 'Insert the list name'
|
|
|
|
),
|
|
|
|
'filter' => array(
|
|
|
|
'name' => 'Filter',
|
|
|
|
'exampleValue' => '#rss-bridge',
|
|
|
|
'required' => false,
|
|
|
|
'title' => 'Specify term to search for'
|
|
|
|
)
|
2020-11-10 09:12:02 +03:00
|
|
|
),
|
|
|
|
'By list ID' => array(
|
|
|
|
'listid' => array(
|
|
|
|
'name' => 'List ID',
|
|
|
|
'exampleValue' => '31748',
|
|
|
|
'required' => true,
|
|
|
|
'title' => 'Insert the list id'
|
|
|
|
),
|
|
|
|
'filter' => array(
|
|
|
|
'name' => 'Filter',
|
|
|
|
'exampleValue' => '#rss-bridge',
|
|
|
|
'required' => false,
|
|
|
|
'title' => 'Specify term to search for'
|
|
|
|
)
|
2017-02-11 18:16:56 +03:00
|
|
|
)
|
|
|
|
);
|
2015-11-05 18:50:18 +03:00
|
|
|
|
2018-11-26 20:05:41 +03:00
|
|
|
public function detectParameters($url){
|
|
|
|
$params = array();
|
|
|
|
|
|
|
|
// By keyword or hashtag (search)
|
|
|
|
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/search.*(\?|&)q=([^\/&?\n]+)/';
|
|
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
|
|
$params['q'] = urldecode($matches[4]);
|
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
// By hashtag
|
|
|
|
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/hashtag\/([^\/?\n]+)/';
|
|
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
|
|
$params['q'] = urldecode($matches[3]);
|
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
// By list
|
|
|
|
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)\/lists\/([^\/?\n]+)/';
|
|
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
|
|
$params['user'] = urldecode($matches[3]);
|
|
|
|
$params['list'] = urldecode($matches[4]);
|
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
// By username
|
|
|
|
$regex = '/^(https?:\/\/)?(www\.)?twitter\.com\/([^\/?\n]+)/';
|
|
|
|
if(preg_match($regex, $url, $matches) > 0) {
|
|
|
|
$params['u'] = urldecode($matches[3]);
|
|
|
|
return $params;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2017-02-11 18:16:56 +03:00
|
|
|
public function getName(){
|
2017-07-29 20:28:00 +03:00
|
|
|
switch($this->queriedContext) {
|
2017-02-11 18:16:56 +03:00
|
|
|
case 'By keyword or hashtag':
|
|
|
|
$specific = 'search ';
|
|
|
|
$param = 'q';
|
|
|
|
break;
|
|
|
|
case 'By username':
|
|
|
|
$specific = '@';
|
|
|
|
$param = 'u';
|
|
|
|
break;
|
2017-09-24 17:59:45 +03:00
|
|
|
case 'By list':
|
2018-01-12 15:07:40 +03:00
|
|
|
return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
|
2020-11-10 09:12:02 +03:00
|
|
|
case 'By list ID':
|
|
|
|
return 'Twitter List #' . $this->getInput('listid');
|
2017-02-15 00:20:55 +03:00
|
|
|
default: return parent::getName();
|
2017-02-11 18:16:56 +03:00
|
|
|
}
|
|
|
|
return 'Twitter ' . $specific . $this->getInput($param);
|
|
|
|
}
|
2016-08-25 02:49:30 +03:00
|
|
|
|
2017-02-11 18:16:56 +03:00
|
|
|
public function getURI(){
|
2017-07-29 20:28:00 +03:00
|
|
|
switch($this->queriedContext) {
|
2017-02-11 18:16:56 +03:00
|
|
|
case 'By keyword or hashtag':
|
|
|
|
return self::URI
|
|
|
|
. 'search?q='
|
|
|
|
. urlencode($this->getInput('q'))
|
|
|
|
. '&f=tweets';
|
|
|
|
case 'By username':
|
|
|
|
return self::URI
|
2017-07-03 20:52:56 +03:00
|
|
|
. urlencode($this->getInput('u'));
|
|
|
|
// Always return without replies!
|
|
|
|
// . ($this->getInput('norep') ? '' : '/with_replies');
|
2017-09-24 17:59:45 +03:00
|
|
|
case 'By list':
|
|
|
|
return self::URI
|
|
|
|
. urlencode($this->getInput('user'))
|
|
|
|
. '/lists/'
|
|
|
|
. str_replace(' ', '-', strtolower($this->getInput('list')));
|
2020-11-10 09:12:02 +03:00
|
|
|
case 'By list ID':
|
|
|
|
return self::URI
|
|
|
|
. 'i/lists/'
|
|
|
|
. urlencode($this->getInput('listid'));
|
2017-02-15 00:36:33 +03:00
|
|
|
default: return parent::getURI();
|
2017-02-11 18:16:56 +03:00
|
|
|
}
|
|
|
|
}
|
2016-08-25 02:49:30 +03:00
|
|
|
|
2020-06-08 12:18:24 +03:00
|
|
|
private function getApiURI() {
|
2020-06-05 11:17:53 +03:00
|
|
|
switch($this->queriedContext) {
|
|
|
|
case 'By keyword or hashtag':
|
|
|
|
return self::API_URI
|
|
|
|
. '/2/search/adaptive.json?q='
|
|
|
|
. urlencode($this->getInput('q'))
|
2020-08-02 17:40:41 +03:00
|
|
|
. '&tweet_mode=extended&tweet_search_mode=live';
|
2020-06-05 11:17:53 +03:00
|
|
|
case 'By username':
|
2021-04-12 21:08:38 +03:00
|
|
|
// use search endpoint if without replies or without retweets enabled
|
|
|
|
if ($this->getInput('noretweet') || $this->getInput('norep')) {
|
|
|
|
$query = 'from:' . $this->getInput('u');
|
|
|
|
// Twitter's from: search excludes retweets by default
|
|
|
|
if (!$this->getInput('noretweet')) $query .= ' include:nativeretweets';
|
|
|
|
if ($this->getInput('norep')) $query .= ' exclude:replies';
|
|
|
|
return self::API_URI
|
|
|
|
. '/2/search/adaptive.json?q='
|
|
|
|
. urlencode($query)
|
|
|
|
. '&tweet_mode=extended&tweet_search_mode=live';
|
|
|
|
} else {
|
|
|
|
return self::API_URI
|
|
|
|
. '/2/timeline/profile/'
|
|
|
|
. $this->getRestId($this->getInput('u'))
|
|
|
|
. '.json?tweet_mode=extended';
|
|
|
|
}
|
2020-06-05 11:17:53 +03:00
|
|
|
case 'By list':
|
|
|
|
return self::API_URI
|
|
|
|
. '/2/timeline/list.json?list_id='
|
|
|
|
. $this->getListId($this->getInput('user'), $this->getInput('list'))
|
|
|
|
. '&tweet_mode=extended';
|
2020-11-10 09:12:02 +03:00
|
|
|
case 'By list ID':
|
|
|
|
return self::API_URI
|
|
|
|
. '/2/timeline/list.json?list_id='
|
|
|
|
. $this->getInput('listid')
|
|
|
|
. '&tweet_mode=extended';
|
2020-06-05 11:17:53 +03:00
|
|
|
default: returnServerError('Invalid query context !');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-08-25 02:24:53 +03:00
|
|
|
public function collectData(){
|
2016-07-08 20:06:35 +03:00
|
|
|
$html = '';
|
2019-07-26 11:36:59 +03:00
|
|
|
$page = $this->getURI();
|
2020-06-05 11:17:53 +03:00
|
|
|
$data = json_decode($this->getApiContents($this->getApiURI()));
|
2019-09-12 17:14:48 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
if(!$data) {
|
2017-07-29 20:28:00 +03:00
|
|
|
switch($this->queriedContext) {
|
2016-08-28 01:01:42 +03:00
|
|
|
case 'By keyword or hashtag':
|
2016-09-26 00:22:33 +03:00
|
|
|
returnServerError('No results for this query.');
|
2016-08-28 01:01:42 +03:00
|
|
|
case 'By username':
|
2016-09-26 00:22:33 +03:00
|
|
|
returnServerError('Requested username can\'t be found.');
|
2017-09-24 17:59:45 +03:00
|
|
|
case 'By list':
|
|
|
|
returnServerError('Requested username or list can\'t be found');
|
2016-08-28 01:01:42 +03:00
|
|
|
}
|
2014-05-26 02:30:46 +04:00
|
|
|
}
|
|
|
|
|
2016-08-28 02:25:33 +03:00
|
|
|
$hidePictures = $this->getInput('nopic');
|
2016-08-10 11:44:23 +03:00
|
|
|
|
2020-09-11 09:44:28 +03:00
|
|
|
$promotedTweetIds = array_reduce($data->timeline->instructions[0]->addEntries->entries, function($carry, $entry) {
|
|
|
|
if (!isset($entry->content->item)) {
|
|
|
|
return $carry;
|
|
|
|
}
|
|
|
|
$tweet = $entry->content->item->content->tweet;
|
|
|
|
if (isset($tweet->promotedMetadata)) {
|
|
|
|
$carry[] = $tweet->id;
|
|
|
|
}
|
|
|
|
return $carry;
|
|
|
|
}, array());
|
|
|
|
|
2021-01-10 11:50:06 +03:00
|
|
|
$hidePinned = $this->getInput('nopinned');
|
|
|
|
if ($hidePinned) {
|
|
|
|
$pinnedTweetId = null;
|
|
|
|
if (isset($data->timeline->instructions[1]) && isset($data->timeline->instructions[1]->pinEntry)) {
|
|
|
|
$pinnedTweetId = $data->timeline->instructions[1]->pinEntry->entry->content->item->content->tweet->id;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-12 21:08:38 +03:00
|
|
|
$tweets = array();
|
|
|
|
|
|
|
|
// Extract tweets from timeline property when in username mode
|
|
|
|
// This fixes number of issues:
|
|
|
|
// * If there's a retweet of a quote tweet, the quoted tweet will not appear in results (since it wasn't retweeted directly)
|
|
|
|
// * Pinned tweets do not get stuck at the bottom
|
|
|
|
if ($this->queriedContext === 'By username') {
|
|
|
|
foreach($data->timeline->instructions[0]->addEntries->entries as $tweet) {
|
|
|
|
if (!isset($tweet->content->item)) continue;
|
|
|
|
$tweetId = $tweet->content->item->content->tweet->id;
|
|
|
|
$selectedTweet = $this->getTweet($tweetId, $data->globalObjects);
|
|
|
|
if (!$selectedTweet) continue;
|
|
|
|
// If this is a retweet, it will contain shorter text and will point to the original full tweet (retweeted_status_id_str).
|
|
|
|
// Let's use the original tweet text.
|
|
|
|
if (isset($selectedTweet->retweeted_status_id_str)) {
|
|
|
|
$tweetId = $selectedTweet->retweeted_status_id_str;
|
|
|
|
$selectedTweet = $this->getTweet($tweetId, $data->globalObjects);
|
|
|
|
if (!$selectedTweet) continue;
|
|
|
|
}
|
|
|
|
// use $tweetId as key to avoid duplicates (e.g. user retweeting their own tweet)
|
|
|
|
$tweets[$tweetId] = $selectedTweet;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
foreach($data->globalObjects->tweets as $tweet) {
|
|
|
|
$tweets[] = $tweet;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach($tweets as $tweet) {
|
2017-03-17 20:41:35 +03:00
|
|
|
|
2020-07-26 09:26:39 +03:00
|
|
|
/* Debug::log('>>> ' . json_encode($tweet)); */
|
|
|
|
// Skip spurious retweets
|
|
|
|
if (isset($tweet->retweeted_status_id_str) && substr($tweet->full_text, 0, 4) === 'RT @') {
|
2017-08-03 01:26:32 +03:00
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2020-09-11 09:44:28 +03:00
|
|
|
// Skip promoted tweets
|
|
|
|
if (in_array($tweet->id_str, $promotedTweetIds)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2021-01-10 11:50:06 +03:00
|
|
|
// Skip pinned tweet
|
|
|
|
if ($hidePinned && $tweet->id_str === $pinnedTweetId) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2016-08-22 19:55:59 +03:00
|
|
|
$item = array();
|
2014-05-26 02:30:46 +04:00
|
|
|
// extract username and sanitize
|
2020-06-05 11:17:53 +03:00
|
|
|
$user_info = $this->getUserInformation($tweet->user_id_str, $data->globalObjects);
|
2017-09-24 17:59:45 +03:00
|
|
|
|
2020-06-08 12:18:24 +03:00
|
|
|
$item['username'] = $user_info->screen_name;
|
|
|
|
$item['fullname'] = $user_info->name;
|
2020-06-05 11:17:53 +03:00
|
|
|
$item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
|
2021-01-10 11:19:38 +03:00
|
|
|
if (null !== $this->getInput('u') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
|
2020-08-20 08:00:27 +03:00
|
|
|
$item['author'] .= ' RT: @' . $this->getInput('u');
|
|
|
|
}
|
2020-06-05 11:17:53 +03:00
|
|
|
$item['avatar'] = $user_info->profile_image_url_https;
|
2016-08-09 22:47:29 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
$item['id'] = $tweet->id_str;
|
2020-06-10 23:39:36 +03:00
|
|
|
$item['uri'] = self::URI . $item['username'] . '/status/' . $item['id'];
|
2020-06-05 11:17:53 +03:00
|
|
|
// extract tweet timestamp
|
|
|
|
$item['timestamp'] = $tweet->created_at;
|
2016-08-09 22:47:29 +03:00
|
|
|
|
2020-08-21 15:55:11 +03:00
|
|
|
// Convert plain text URLs into HTML hyperlinks
|
|
|
|
$cleanedTweet = $tweet->full_text;
|
|
|
|
$foundUrls = false;
|
|
|
|
|
|
|
|
if (isset($tweet->entities->media)) {
|
|
|
|
foreach($tweet->entities->media as $media) {
|
|
|
|
$cleanedTweet = str_replace($media->url,
|
|
|
|
'<a href="' . $media->expanded_url . '">' . $media->display_url . '</a>',
|
|
|
|
$cleanedTweet);
|
|
|
|
$foundUrls = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (isset($tweet->entities->urls)) {
|
|
|
|
foreach($tweet->entities->urls as $url) {
|
|
|
|
$cleanedTweet = str_replace($url->url,
|
|
|
|
'<a href="' . $url->expanded_url . '">' . $url->display_url . '</a>',
|
|
|
|
$cleanedTweet);
|
|
|
|
$foundUrls = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if ($foundUrls === false) {
|
|
|
|
// fallback to regex'es
|
|
|
|
$reg_ex = '/(http|https|ftp|ftps)\:\/\/[a-zA-Z0-9\-\.]+\.[a-zA-Z]{2,3}(\/\S*)?/';
|
|
|
|
if(preg_match($reg_ex, $tweet->full_text, $url)) {
|
|
|
|
$cleanedTweet = preg_replace($reg_ex,
|
|
|
|
"<a href='{$url[0]}' target='_blank'>{$url[0]}</a> ",
|
|
|
|
$cleanedTweet);
|
|
|
|
}
|
|
|
|
}
|
2020-06-05 11:17:53 +03:00
|
|
|
// generate the title
|
2020-08-21 15:55:11 +03:00
|
|
|
$item['title'] = strip_tags($cleanedTweet);
|
2017-06-19 00:18:41 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
// Add avatar
|
2016-08-10 11:44:23 +03:00
|
|
|
$picture_html = '';
|
2017-07-29 20:28:00 +03:00
|
|
|
if(!$hidePictures) {
|
2016-08-10 11:44:23 +03:00
|
|
|
$picture_html = <<<EOD
|
2017-02-11 18:16:56 +03:00
|
|
|
<a href="https://twitter.com/{$item['username']}">
|
|
|
|
<img
|
2017-04-22 16:36:25 +03:00
|
|
|
style="align:top; width:75px; border:1px solid black;"
|
2017-02-11 18:16:56 +03:00
|
|
|
alt="{$item['username']}"
|
|
|
|
src="{$item['avatar']}"
|
|
|
|
title="{$item['fullname']}" />
|
|
|
|
</a>
|
2016-08-10 11:44:23 +03:00
|
|
|
EOD;
|
|
|
|
}
|
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
// Get images
|
2020-06-10 23:39:36 +03:00
|
|
|
$media_html = '';
|
2020-06-05 11:17:53 +03:00
|
|
|
if(isset($tweet->extended_entities->media) && !$this->getInput('noimg')) {
|
|
|
|
foreach($tweet->extended_entities->media as $media) {
|
2020-06-10 23:39:36 +03:00
|
|
|
switch($media->type) {
|
|
|
|
case 'photo':
|
|
|
|
$image = $media->media_url_https . '?name=orig';
|
|
|
|
$display_image = $media->media_url_https;
|
|
|
|
// add enclosures
|
|
|
|
$item['enclosures'][] = $image;
|
|
|
|
|
|
|
|
$media_html .= <<<EOD
|
2020-06-05 11:17:53 +03:00
|
|
|
<a href="{$image}">
|
2017-04-22 16:36:25 +03:00
|
|
|
<img
|
|
|
|
style="align:top; max-width:558px; border:1px solid black;"
|
2020-06-08 12:18:24 +03:00
|
|
|
referrerpolicy="no-referrer"
|
2020-06-05 11:17:53 +03:00
|
|
|
src="{$display_image}" />
|
2017-04-22 16:36:25 +03:00
|
|
|
</a>
|
|
|
|
EOD;
|
2020-06-10 23:39:36 +03:00
|
|
|
break;
|
|
|
|
case 'video':
|
|
|
|
case 'animated_gif':
|
|
|
|
if(isset($media->video_info)) {
|
|
|
|
$link = $media->expanded_url;
|
|
|
|
$poster = $media->media_url_https;
|
|
|
|
$video = null;
|
|
|
|
$maxBitrate = -1;
|
|
|
|
foreach($media->video_info->variants as $variant) {
|
|
|
|
$bitRate = isset($variant->bitrate) ? $variant->bitrate : -100;
|
|
|
|
if ($bitRate > $maxBitrate) {
|
|
|
|
$maxBitrate = $bitRate;
|
|
|
|
$video = $variant->url;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if(!is_null($video)) {
|
|
|
|
// add enclosures
|
|
|
|
$item['enclosures'][] = $video;
|
|
|
|
$item['enclosures'][] = $poster;
|
|
|
|
|
|
|
|
$media_html .= <<<EOD
|
|
|
|
<a href="{$link}">Video</a>
|
|
|
|
<video
|
|
|
|
style="align:top; max-width:558px; border:1px solid black;"
|
|
|
|
referrerpolicy="no-referrer"
|
|
|
|
src="{$video}" poster="{$poster}" />
|
|
|
|
EOD;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
Debug::log('Missing support for media type: ' . $media->type);
|
|
|
|
}
|
2019-06-09 18:24:40 +03:00
|
|
|
}
|
2017-04-22 16:36:25 +03:00
|
|
|
}
|
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
switch($this->queriedContext) {
|
|
|
|
case 'By list':
|
2020-11-10 09:12:02 +03:00
|
|
|
case 'By list ID':
|
2020-06-05 11:17:53 +03:00
|
|
|
// Check if filter applies to list (using raw content)
|
|
|
|
if($this->getInput('filter')) {
|
|
|
|
if(stripos($cleanedTweet, $this->getInput('filter')) === false) {
|
|
|
|
continue 2; // switch + for-loop!
|
|
|
|
}
|
|
|
|
}
|
|
|
|
break;
|
2020-07-26 09:26:39 +03:00
|
|
|
case 'By username':
|
2021-01-10 11:19:38 +03:00
|
|
|
if ($this->getInput('noretweet') && strtolower($item['username']) != strtolower($this->getInput('u'))) {
|
2020-07-26 09:26:39 +03:00
|
|
|
continue 2; // switch + for-loop!
|
|
|
|
}
|
|
|
|
break;
|
2020-06-05 11:17:53 +03:00
|
|
|
default:
|
|
|
|
}
|
|
|
|
|
2016-08-22 19:55:59 +03:00
|
|
|
$item['content'] = <<<EOD
|
2016-08-09 23:05:42 +03:00
|
|
|
<div style="display: inline-block; vertical-align: top;">
|
2016-08-10 11:44:23 +03:00
|
|
|
{$picture_html}
|
2016-08-09 23:05:42 +03:00
|
|
|
</div>
|
|
|
|
<div style="display: inline-block; vertical-align: top;">
|
|
|
|
<blockquote>{$cleanedTweet}</blockquote>
|
|
|
|
</div>
|
2017-04-22 16:36:25 +03:00
|
|
|
<div style="display: block; vertical-align: top;">
|
2020-06-10 23:39:36 +03:00
|
|
|
<blockquote>{$media_html}</blockquote>
|
2017-04-22 16:36:25 +03:00
|
|
|
</div>
|
|
|
|
EOD;
|
|
|
|
|
2018-11-16 00:00:01 +03:00
|
|
|
$item['content'] = htmlspecialchars_decode($item['content'], ENT_QUOTES);
|
2016-08-09 22:47:29 +03:00
|
|
|
|
2014-05-26 02:30:46 +04:00
|
|
|
// put out
|
|
|
|
$this->items[] = $item;
|
|
|
|
}
|
2017-04-22 16:36:25 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
usort($this->items, array('TwitterBridge', 'compareTweetId'));
|
2017-04-22 16:36:25 +03:00
|
|
|
}
|
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
private static function compareTweetId($tweet1, $tweet2) {
|
|
|
|
return (intval($tweet1['id']) < intval($tweet2['id']) ? 1 : -1);
|
2017-06-19 00:18:41 +03:00
|
|
|
}
|
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
//The aim of this function is to get an API key and a guest token
|
|
|
|
//This function takes 2 requests, and therefore is cached
|
|
|
|
private function getApiKey() {
|
|
|
|
|
2020-06-23 16:14:50 +03:00
|
|
|
$cacheFac = new CacheFactory();
|
|
|
|
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
|
|
|
|
$r_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
|
|
|
|
$r_cache->setScope(get_called_class());
|
|
|
|
$r_cache->setKey(array('refresh'));
|
|
|
|
$data = $r_cache->loadData();
|
|
|
|
|
|
|
|
$refresh = null;
|
|
|
|
if($data === null) {
|
|
|
|
$refresh = time();
|
|
|
|
$r_cache->saveData($refresh);
|
|
|
|
} else {
|
|
|
|
$refresh = $data;
|
|
|
|
}
|
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
$cacheFac = new CacheFactory();
|
|
|
|
$cacheFac->setWorkingDir(PATH_LIB_CACHES);
|
|
|
|
$cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
|
|
|
|
$cache->setScope(get_called_class());
|
|
|
|
$cache->setKey(array('api_key'));
|
|
|
|
$data = $cache->loadData();
|
|
|
|
|
2020-06-08 12:18:24 +03:00
|
|
|
$apiKey = null;
|
2020-07-30 07:54:16 +03:00
|
|
|
if($data === null || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
|
2020-06-05 11:17:53 +03:00
|
|
|
$twitterPage = getContents('https://twitter.com');
|
2020-06-08 12:18:24 +03:00
|
|
|
|
2020-10-05 10:07:39 +03:00
|
|
|
$jsLink = false;
|
|
|
|
$jsMainRegexArray = array(
|
|
|
|
'/(https:\/\/abs\.twimg\.com\/responsive-web\/web\/main\.[^\.]+\.js)/m',
|
|
|
|
'/(https:\/\/abs\.twimg\.com\/responsive-web\/web_legacy\/main\.[^\.]+\.js)/m',
|
|
|
|
'/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js)/m',
|
|
|
|
'/(https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js)/m',
|
|
|
|
);
|
|
|
|
foreach ($jsMainRegexArray as $jsMainRegex) {
|
|
|
|
if (preg_match_all($jsMainRegex, $twitterPage, $jsMainMatches, PREG_SET_ORDER, 0)) {
|
|
|
|
$jsLink = $jsMainMatches[0][0];
|
|
|
|
break;
|
|
|
|
}
|
2020-09-28 08:01:37 +03:00
|
|
|
}
|
2020-10-05 10:07:39 +03:00
|
|
|
if (!$jsLink) {
|
2020-08-06 08:22:17 +03:00
|
|
|
returnServerError('Could not locate main.js link');
|
|
|
|
}
|
2020-06-05 11:17:53 +03:00
|
|
|
|
|
|
|
$jsContent = getContents($jsLink);
|
|
|
|
$apiKeyRegex = '/([a-zA-Z0-9]{59}%[a-zA-Z0-9]{44})/m';
|
|
|
|
preg_match_all($apiKeyRegex, $jsContent, $apiKeyMatches, PREG_SET_ORDER, 0);
|
|
|
|
$apiKey = $apiKeyMatches[0][0];
|
2020-06-08 12:18:24 +03:00
|
|
|
$cache->saveData($apiKey);
|
|
|
|
} else {
|
|
|
|
$apiKey = $data;
|
|
|
|
}
|
|
|
|
|
|
|
|
$cacheFac2 = new CacheFactory();
|
|
|
|
$cacheFac2->setWorkingDir(PATH_LIB_CACHES);
|
|
|
|
$gt_cache = $cacheFac->create(Configuration::getConfig('cache', 'type'));
|
|
|
|
$gt_cache->setScope(get_called_class());
|
|
|
|
$gt_cache->setKey(array('guest_token'));
|
|
|
|
$guestTokenUses = $gt_cache->loadData();
|
|
|
|
|
|
|
|
$guestToken = null;
|
2020-06-23 16:14:50 +03:00
|
|
|
if($guestTokenUses === null || !is_array($guestTokenUses) || count($guestTokenUses) != 2
|
|
|
|
|| $guestTokenUses[0] <= 0 || (time() - $refresh) > self::GUEST_TOKEN_EXPIRY) {
|
2020-06-08 12:18:24 +03:00
|
|
|
$guestToken = $this->getGuestToken();
|
|
|
|
$gt_cache->saveData(array(self::GUEST_TOKEN_USES, $guestToken));
|
2020-06-23 16:14:50 +03:00
|
|
|
$r_cache->saveData(time());
|
2020-06-08 12:18:24 +03:00
|
|
|
} else {
|
|
|
|
$guestTokenUses[0] -= 1;
|
|
|
|
$gt_cache->saveData($guestTokenUses);
|
|
|
|
$guestToken = $guestTokenUses[1];
|
2019-06-09 18:24:40 +03:00
|
|
|
}
|
|
|
|
|
2020-06-08 12:18:24 +03:00
|
|
|
return array($apiKey, $guestToken);
|
2017-04-22 16:36:25 +03:00
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-06-08 12:18:24 +03:00
|
|
|
// Get a guest token. This is different to an API key,
|
|
|
|
// and it seems to change more regularly than the API key.
|
|
|
|
private function getGuestToken() {
|
2020-06-25 13:21:48 +03:00
|
|
|
$pageContent = getContents('https://twitter.com', array(), array(), true);
|
2020-06-08 12:18:24 +03:00
|
|
|
|
|
|
|
$guestTokenRegex = '/gt=([0-9]*)/m';
|
2020-06-25 13:21:48 +03:00
|
|
|
preg_match_all($guestTokenRegex, $pageContent['header'], $guestTokenMatches, PREG_SET_ORDER, 0);
|
2020-07-24 10:52:27 +03:00
|
|
|
if (!$guestTokenMatches)
|
|
|
|
preg_match_all($guestTokenRegex, $pageContent['content'], $guestTokenMatches, PREG_SET_ORDER, 0);
|
2020-06-25 13:21:48 +03:00
|
|
|
if (!$guestTokenMatches) returnServerError('Could not parse guest token');
|
2020-06-08 12:18:24 +03:00
|
|
|
$guestToken = $guestTokenMatches[0][1];
|
|
|
|
return $guestToken;
|
|
|
|
}
|
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
private function getApiContents($uri) {
|
|
|
|
$apiKeys = $this->getApiKey();
|
|
|
|
$headers = array('authorization: Bearer ' . $apiKeys[0],
|
|
|
|
'x-guest-token: ' . $apiKeys[1],
|
|
|
|
);
|
|
|
|
return getContents($uri, $headers);
|
|
|
|
}
|
2017-04-22 16:36:25 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
private function getRestId($username) {
|
|
|
|
$searchparams = urlencode('{"screen_name":"' . strtolower($username) . '", "withHighlightedLabel":true}');
|
|
|
|
$searchURL = self::API_URI . '/graphql/-xfUfZsnR_zqjFd-IfrN5A/UserByScreenName?variables=' . $searchparams;
|
|
|
|
$searchResult = $this->getApiContents($searchURL);
|
|
|
|
$searchResult = json_decode($searchResult);
|
|
|
|
return $searchResult->data->user->rest_id;
|
2017-04-22 16:36:25 +03:00
|
|
|
}
|
2019-07-26 11:36:59 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
private function getListId($username, $listName) {
|
|
|
|
$searchparams = urlencode('{"screenName":"'
|
|
|
|
. strtolower($username)
|
|
|
|
. '", "listSlug": "'
|
|
|
|
. $listName
|
|
|
|
. '", "withHighlightedLabel":false}');
|
|
|
|
$searchURL = self::API_URI . '/graphql/ErWsz9cObLel1BF-HjuBlA/ListBySlug?variables=' . $searchparams;
|
|
|
|
$searchResult = $this->getApiContents($searchURL);
|
|
|
|
$searchResult = json_decode($searchResult);
|
|
|
|
return $searchResult->data->user_by_screen_name->list->id_str;
|
|
|
|
}
|
2019-07-26 11:36:59 +03:00
|
|
|
|
2020-06-05 11:17:53 +03:00
|
|
|
private function getUserInformation($userId, $apiData) {
|
|
|
|
foreach($apiData->users as $user) {
|
|
|
|
if($user->id_str == $userId) {
|
|
|
|
return $user;
|
2019-07-26 11:36:59 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-04-12 21:08:38 +03:00
|
|
|
|
|
|
|
private function getTweet($tweetId, $apiData) {
|
|
|
|
if (property_exists($apiData->tweets, $tweetId)) {
|
|
|
|
return $apiData->tweets->$tweetId;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2014-05-26 02:30:46 +04:00
|
|
|
}
|