TwitterBridge.php 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327
  1. <?php
  2. class TwitterBridge extends BridgeAbstract {
  3. const NAME = 'Twitter Bridge';
  4. const URI = 'https://twitter.com/';
  5. const CACHE_TIMEOUT = 300; // 5min
  6. const DESCRIPTION = 'returns tweets';
  7. const MAINTAINER = 'pmaziere';
  8. const PARAMETERS = array(
  9. 'global' => array(
  10. 'nopic' => array(
  11. 'name' => 'Hide profile pictures',
  12. 'type' => 'checkbox',
  13. 'title' => 'Activate to hide profile pictures in content'
  14. ),
  15. 'noimg' => array(
  16. 'name' => 'Hide images in tweets',
  17. 'type' => 'checkbox',
  18. 'title' => 'Activate to hide images in tweets'
  19. )
  20. ),
  21. 'By keyword or hashtag' => array(
  22. 'q' => array(
  23. 'name' => 'Keyword or #hashtag',
  24. 'required' => true,
  25. 'exampleValue' => 'rss-bridge, #rss-bridge',
  26. 'title' => 'Insert a keyword or hashtag'
  27. )
  28. ),
  29. 'By username' => array(
  30. 'u' => array(
  31. 'name' => 'username',
  32. 'required' => true,
  33. 'exampleValue' => 'sebsauvage',
  34. 'title' => 'Insert a user name'
  35. ),
  36. 'norep' => array(
  37. 'name' => 'Without replies',
  38. 'type' => 'checkbox',
  39. 'title' => 'Only return initial tweets'
  40. ),
  41. 'noretweet' => array(
  42. 'name' => 'Without retweets',
  43. 'required' => false,
  44. 'type' => 'checkbox',
  45. 'title' => 'Hide retweets'
  46. )
  47. ),
  48. 'By list' => array(
  49. 'user' => array(
  50. 'name' => 'User',
  51. 'required' => true,
  52. 'exampleValue' => 'sebsauvage',
  53. 'title' => 'Insert a user name'
  54. ),
  55. 'list' => array(
  56. 'name' => 'List',
  57. 'required' => true,
  58. 'title' => 'Insert the list name'
  59. ),
  60. 'filter' => array(
  61. 'name' => 'Filter',
  62. 'exampleValue' => '#rss-bridge',
  63. 'required' => false,
  64. 'title' => 'Specify term to search for'
  65. )
  66. )
  67. );
  68. public function getName(){
  69. switch($this->queriedContext) {
  70. case 'By keyword or hashtag':
  71. $specific = 'search ';
  72. $param = 'q';
  73. break;
  74. case 'By username':
  75. $specific = '@';
  76. $param = 'u';
  77. break;
  78. case 'By list':
  79. return $this->getInput('list') . ' - Twitter list by ' . $this->getInput('user');
  80. default: return parent::getName();
  81. }
  82. return 'Twitter ' . $specific . $this->getInput($param);
  83. }
  84. public function getURI(){
  85. switch($this->queriedContext) {
  86. case 'By keyword or hashtag':
  87. return self::URI
  88. . 'search?q='
  89. . urlencode($this->getInput('q'))
  90. . '&f=tweets';
  91. case 'By username':
  92. return self::URI
  93. . urlencode($this->getInput('u'));
  94. // Always return without replies!
  95. // . ($this->getInput('norep') ? '' : '/with_replies');
  96. case 'By list':
  97. return self::URI
  98. . urlencode($this->getInput('user'))
  99. . '/lists/'
  100. . str_replace(' ', '-', strtolower($this->getInput('list')));
  101. default: return parent::getURI();
  102. }
  103. }
  104. public function collectData(){
  105. $html = '';
  106. $html = getSimpleHTMLDOM($this->getURI());
  107. if(!$html) {
  108. switch($this->queriedContext) {
  109. case 'By keyword or hashtag':
  110. returnServerError('No results for this query.');
  111. case 'By username':
  112. returnServerError('Requested username can\'t be found.');
  113. case 'By list':
  114. returnServerError('Requested username or list can\'t be found');
  115. }
  116. }
  117. $hidePictures = $this->getInput('nopic');
  118. foreach($html->find('div.js-stream-tweet') as $tweet) {
  119. // Skip retweets?
  120. if($this->getInput('noretweet')
  121. && $tweet->getAttribute('data-screen-name') !== $this->getInput('u')) {
  122. continue;
  123. }
  124. // remove 'invisible' content
  125. foreach($tweet->find('.invisible') as $invisible) {
  126. $invisible->outertext = '';
  127. }
  128. // Skip protmoted tweets
  129. $heading = $tweet->previousSibling();
  130. if(!is_null($heading) &&
  131. $heading->getAttribute('class') === 'promoted-tweet-heading'
  132. ) {
  133. continue;
  134. }
  135. $item = array();
  136. // extract username and sanitize
  137. $item['username'] = $tweet->getAttribute('data-screen-name');
  138. // extract fullname (pseudonym)
  139. $item['fullname'] = $tweet->getAttribute('data-name');
  140. // get author
  141. $item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
  142. // get avatar link
  143. $item['avatar'] = $tweet->find('img', 0)->src;
  144. // get TweetID
  145. $item['id'] = $tweet->getAttribute('data-tweet-id');
  146. // get tweet link
  147. $item['uri'] = self::URI . substr($tweet->find('a.js-permalink', 0)->getAttribute('href'), 1);
  148. // extract tweet timestamp
  149. $item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time');
  150. // generate the title
  151. $item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
  152. switch($this->queriedContext) {
  153. case 'By list':
  154. // Check if filter applies to list (using raw content)
  155. if($this->getInput('filter')) {
  156. if(stripos($tweet->find('p.js-tweet-text', 0)->plaintext, $this->getInput('filter')) === false) {
  157. continue 2; // switch + for-loop!
  158. }
  159. }
  160. break;
  161. default:
  162. }
  163. $this->processContentLinks($tweet);
  164. $this->processEmojis($tweet);
  165. // get tweet text
  166. $cleanedTweet = str_replace(
  167. 'href="/',
  168. 'href="' . self::URI,
  169. $tweet->find('p.js-tweet-text', 0)->innertext
  170. );
  171. // fix anchors missing spaces in-between
  172. $cleanedTweet = $this->fixAnchorSpacing($cleanedTweet);
  173. // Add picture to content
  174. $picture_html = '';
  175. if(!$hidePictures) {
  176. $picture_html = <<<EOD
  177. <a href="https://twitter.com/{$item['username']}">
  178. <img
  179. style="align:top; width:75px; border:1px solid black;"
  180. alt="{$item['username']}"
  181. src="{$item['avatar']}"
  182. title="{$item['fullname']}" />
  183. </a>
  184. EOD;
  185. }
  186. // Add embeded image to content
  187. $image_html = '';
  188. $image = $this->getImageURI($tweet);
  189. if(!$this->getInput('noimg') && !is_null($image)) {
  190. // add enclosures
  191. $item['enclosures'] = array($image . ':orig');
  192. $image_html = <<<EOD
  193. <a href="{$image}:orig">
  194. <img
  195. style="align:top; max-width:558px; border:1px solid black;"
  196. src="{$image}:thumb" />
  197. </a>
  198. EOD;
  199. }
  200. // add content
  201. $item['content'] = <<<EOD
  202. <div style="display: inline-block; vertical-align: top;">
  203. {$picture_html}
  204. </div>
  205. <div style="display: inline-block; vertical-align: top;">
  206. <blockquote>{$cleanedTweet}</blockquote>
  207. </div>
  208. <div style="display: block; vertical-align: top;">
  209. <blockquote>{$image_html}</blockquote>
  210. </div>
  211. EOD;
  212. // add quoted tweet
  213. $quotedTweet = $tweet->find('div.QuoteTweet', 0);
  214. if($quotedTweet) {
  215. // get tweet text
  216. $cleanedQuotedTweet = str_replace(
  217. 'href="/',
  218. 'href="' . self::URI,
  219. $quotedTweet->find('div.tweet-text', 0)->innertext
  220. );
  221. $this->processContentLinks($quotedTweet);
  222. $this->processEmojis($quotedTweet);
  223. // Add embeded image to content
  224. $quotedImage_html = '';
  225. $quotedImage = $this->getQuotedImageURI($tweet);
  226. if(!$this->getInput('noimg') && !is_null($quotedImage)) {
  227. // add enclosures
  228. $item['enclosures'] = array($quotedImage . ':orig');
  229. $quotedImage_html = <<<EOD
  230. <a href="{$quotedImage}:orig">
  231. <img
  232. style="align:top; max-width:558px; border:1px solid black;"
  233. src="{$quotedImage}:thumb" />
  234. </a>
  235. EOD;
  236. }
  237. $item['content'] = <<<EOD
  238. <div style="display: inline-block; vertical-align: top;">
  239. <blockquote>{$cleanedQuotedTweet}</blockquote>
  240. </div>
  241. <div style="display: block; vertical-align: top;">
  242. <blockquote>{$quotedImage_html}</blockquote>
  243. </div>
  244. <hr>
  245. {$item['content']}
  246. EOD;
  247. }
  248. // put out
  249. $this->items[] = $item;
  250. }
  251. }
  252. private function processEmojis($tweet){
  253. // process emojis (reduce size)
  254. foreach($tweet->find('img.Emoji') as $img) {
  255. $img->style .= ' height: 1em;';
  256. }
  257. }
  258. private function processContentLinks($tweet){
  259. // processing content links
  260. foreach($tweet->find('a') as $link) {
  261. if($link->hasAttribute('data-expanded-url')) {
  262. $link->href = $link->getAttribute('data-expanded-url');
  263. }
  264. $link->removeAttribute('data-expanded-url');
  265. $link->removeAttribute('data-query-source');
  266. $link->removeAttribute('rel');
  267. $link->removeAttribute('class');
  268. $link->removeAttribute('target');
  269. $link->removeAttribute('title');
  270. }
  271. }
  272. private function fixAnchorSpacing($content){
  273. // fix anchors missing spaces in-between
  274. return str_replace(
  275. '<a',
  276. ' <a',
  277. $content
  278. );
  279. }
  280. private function getImageURI($tweet){
  281. // Find media in tweet
  282. $container = $tweet->find('div.AdaptiveMedia-container', 0);
  283. if($container && $container->find('img', 0)) {
  284. return $container->find('img', 0)->src;
  285. }
  286. return null;
  287. }
  288. private function getQuotedImageURI($tweet){
  289. // Find media in tweet
  290. $container = $tweet->find('div.QuoteMedia-container', 0);
  291. if($container && $container->find('img', 0)) {
  292. return $container->find('img', 0)->src;
  293. }
  294. return null;
  295. }
  296. }