TwitterBridge.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  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. );
  49. public function getName(){
  50. switch($this->queriedContext) {
  51. case 'By keyword or hashtag':
  52. $specific = 'search ';
  53. $param = 'q';
  54. break;
  55. case 'By username':
  56. $specific = '@';
  57. $param = 'u';
  58. break;
  59. default: return parent::getName();
  60. }
  61. return 'Twitter ' . $specific . $this->getInput($param);
  62. }
  63. public function getURI(){
  64. switch($this->queriedContext) {
  65. case 'By keyword or hashtag':
  66. return self::URI
  67. . 'search?q='
  68. . urlencode($this->getInput('q'))
  69. . '&f=tweets';
  70. case 'By username':
  71. return self::URI
  72. . urlencode($this->getInput('u'));
  73. // Always return without replies!
  74. // . ($this->getInput('norep') ? '' : '/with_replies');
  75. default: return parent::getURI();
  76. }
  77. }
  78. public function collectData(){
  79. $html = '';
  80. $html = getSimpleHTMLDOM($this->getURI());
  81. if(!$html) {
  82. switch($this->queriedContext) {
  83. case 'By keyword or hashtag':
  84. returnServerError('No results for this query.');
  85. case 'By username':
  86. returnServerError('Requested username can\'t be found.');
  87. }
  88. }
  89. $hidePictures = $this->getInput('nopic');
  90. foreach($html->find('div.js-stream-tweet') as $tweet) {
  91. // Skip retweets?
  92. if($this->getInput('noretweet')
  93. && $tweet->getAttribute('data-screen-name') !== $this->getInput('u')) {
  94. continue;
  95. }
  96. // remove 'invisible' content
  97. foreach($tweet->find('.invisible') as $invisible) {
  98. $invisible->outertext = '';
  99. }
  100. // Skip protmoted tweets
  101. $heading = $tweet->previousSibling();
  102. if(!is_null($heading) &&
  103. $heading->getAttribute('class') === 'promoted-tweet-heading'
  104. ) {
  105. continue;
  106. }
  107. $item = array();
  108. // extract username and sanitize
  109. $item['username'] = $tweet->getAttribute('data-screen-name');
  110. // extract fullname (pseudonym)
  111. $item['fullname'] = $tweet->getAttribute('data-name');
  112. // get author
  113. $item['author'] = $item['fullname'] . ' (@' . $item['username'] . ')';
  114. // get avatar link
  115. $item['avatar'] = $tweet->find('img', 0)->src;
  116. // get TweetID
  117. $item['id'] = $tweet->getAttribute('data-tweet-id');
  118. // get tweet link
  119. $item['uri'] = self::URI . substr($tweet->find('a.js-permalink', 0)->getAttribute('href'), 1);
  120. // extract tweet timestamp
  121. $item['timestamp'] = $tweet->find('span.js-short-timestamp', 0)->getAttribute('data-time');
  122. // generate the title
  123. $item['title'] = strip_tags($this->fixAnchorSpacing($tweet->find('p.js-tweet-text', 0), '<a>'));
  124. $this->processContentLinks($tweet);
  125. $this->processEmojis($tweet);
  126. // get tweet text
  127. $cleanedTweet = str_replace(
  128. 'href="/',
  129. 'href="' . self::URI,
  130. $tweet->find('p.js-tweet-text', 0)->innertext
  131. );
  132. // fix anchors missing spaces in-between
  133. $cleanedTweet = $this->fixAnchorSpacing($cleanedTweet);
  134. // Add picture to content
  135. $picture_html = '';
  136. if(!$hidePictures) {
  137. $picture_html = <<<EOD
  138. <a href="https://twitter.com/{$item['username']}">
  139. <img
  140. style="align:top; width:75px; border:1px solid black;"
  141. alt="{$item['username']}"
  142. src="{$item['avatar']}"
  143. title="{$item['fullname']}" />
  144. </a>
  145. EOD;
  146. }
  147. // Add embeded image to content
  148. $image_html = '';
  149. $image = $this->getImageURI($tweet);
  150. if(!$this->getInput('noimg') && !is_null($image)) {
  151. // add enclosures
  152. $item['enclosures'] = array($image . ':orig');
  153. $image_html = <<<EOD
  154. <a href="{$image}:orig">
  155. <img
  156. style="align:top; max-width:558px; border:1px solid black;"
  157. src="{$image}:thumb" />
  158. </a>
  159. EOD;
  160. }
  161. // add content
  162. $item['content'] = <<<EOD
  163. <div style="display: inline-block; vertical-align: top;">
  164. {$picture_html}
  165. </div>
  166. <div style="display: inline-block; vertical-align: top;">
  167. <blockquote>{$cleanedTweet}</blockquote>
  168. </div>
  169. <div style="display: block; vertical-align: top;">
  170. <blockquote>{$image_html}</blockquote>
  171. </div>
  172. EOD;
  173. // add quoted tweet
  174. $quotedTweet = $tweet->find('div.QuoteTweet', 0);
  175. if($quotedTweet) {
  176. // get tweet text
  177. $cleanedQuotedTweet = str_replace(
  178. 'href="/',
  179. 'href="' . self::URI,
  180. $quotedTweet->find('div.tweet-text', 0)->innertext
  181. );
  182. $this->processContentLinks($quotedTweet);
  183. $this->processEmojis($quotedTweet);
  184. // Add embeded image to content
  185. $quotedImage_html = '';
  186. $quotedImage = $this->getQuotedImageURI($tweet);
  187. if(!$this->getInput('noimg') && !is_null($quotedImage)) {
  188. // add enclosures
  189. $item['enclosures'] = array($quotedImage . ':orig');
  190. $quotedImage_html = <<<EOD
  191. <a href="{$quotedImage}:orig">
  192. <img
  193. style="align:top; max-width:558px; border:1px solid black;"
  194. src="{$quotedImage}:thumb" />
  195. </a>
  196. EOD;
  197. }
  198. $item['content'] = <<<EOD
  199. <div style="display: inline-block; vertical-align: top;">
  200. <blockquote>{$cleanedQuotedTweet}</blockquote>
  201. </div>
  202. <div style="display: block; vertical-align: top;">
  203. <blockquote>{$quotedImage_html}</blockquote>
  204. </div>
  205. <hr>
  206. {$item['content']}
  207. EOD;
  208. }
  209. // put out
  210. $this->items[] = $item;
  211. }
  212. }
  213. private function processEmojis($tweet){
  214. // process emojis (reduce size)
  215. foreach($tweet->find('img.Emoji') as $img) {
  216. $img->style .= ' height: 1em;';
  217. }
  218. }
  219. private function processContentLinks($tweet){
  220. // processing content links
  221. foreach($tweet->find('a') as $link) {
  222. if($link->hasAttribute('data-expanded-url')) {
  223. $link->href = $link->getAttribute('data-expanded-url');
  224. }
  225. $link->removeAttribute('data-expanded-url');
  226. $link->removeAttribute('data-query-source');
  227. $link->removeAttribute('rel');
  228. $link->removeAttribute('class');
  229. $link->removeAttribute('target');
  230. $link->removeAttribute('title');
  231. }
  232. }
  233. private function fixAnchorSpacing($content){
  234. // fix anchors missing spaces in-between
  235. return str_replace(
  236. '<a',
  237. ' <a',
  238. $content
  239. );
  240. }
  241. private function getImageURI($tweet){
  242. // Find media in tweet
  243. $container = $tweet->find('div.AdaptiveMedia-container', 0);
  244. if($container && $container->find('img', 0)) {
  245. return $container->find('img', 0)->src;
  246. }
  247. return null;
  248. }
  249. private function getQuotedImageURI($tweet){
  250. // Find media in tweet
  251. $container = $tweet->find('div.QuoteMedia-container', 0);
  252. if($container && $container->find('img', 0)) {
  253. return $container->find('img', 0)->src;
  254. }
  255. return null;
  256. }
  257. }