1: <?php
2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35:
36:
37: Yii::import ('application.components.TwitterAPI.TwitterAPIExchange');
38:
39: 40: 41: 42: 43:
44: class extends SortableWidget {
45:
46: public $viewFile = '_twitterFeedWidget';
47:
48: public $model;
49:
50: public $sortableWidgetJSClass = 'TwitterFeedWidget';
51:
52: public $template = '<div class="submenu-title-bar widget-title-bar">{twitterLogo}{widgetLabel}{screenNameSelector}{closeButton}{minimizeButton}</div>{widgetContents}';
53:
54: private static $_JSONPropertiesStructure;
55:
56: private $_username;
57:
58: public static function getJSONPropertiesStructure () {
59: if (!isset (self::$_JSONPropertiesStructure)) {
60: self::$_JSONPropertiesStructure = array_merge (
61: parent::getJSONPropertiesStructure (),
62: array (
63: 'label' => 'Twitter Feed',
64: 'hidden' => false,
65: )
66: );
67: }
68: return self::$_JSONPropertiesStructure;
69: }
70:
71: private ;
72: public function () {
73: if (!isset ($this->_modelTwitterAliases)) {
74: $this->_modelTwitterAliases = RecordAliases::getAliases ($this->model, 'twitter');
75: }
76: return $this->_modelTwitterAliases;
77: }
78:
79: public function () {
80: echo '<span id="twitter-widget-top-bar-logo"></span>';
81: }
82:
83: public function renderScreenNameSelector () {
84: $options = array ();
85: foreach ($this->getModelTwitterAliases () as $alias) {
86: $options[$alias->alias] = $alias->alias;
87: }
88: echo CHtml::dropDownList ('screenName', null, $options, array (
89: 'class' => 'x2-minimal-select',
90: 'id' => 'screen-name-selector',
91: ));
92: }
93:
94: 95: 96:
97: public function getPackages () {
98: if (!isset ($this->_packages)) {
99: $this->_packages = array_merge (
100: parent::getPackages (),
101: array (
102: 'TwitterFeedWidgetJS' => array(
103: 'baseUrl' => Yii::app()->request->baseUrl,
104: 'js' => array (
105: 'js/sortableWidgets/TwitterFeedWidget.js',
106: ),
107: 'depends' => array ('SortableWidgetJS')
108: ),
109: )
110: );
111: }
112: return $this->_packages;
113: }
114:
115: public function getViewFileParams () {
116: if (!isset ($this->_viewFileParams)) {
117: $this->_viewFileParams = array_merge (
118: parent::getViewFileParams (),
119: array (
120: 'username' => $this->_username,
121: )
122: );
123: }
124: return $this->_viewFileParams;
125: }
126:
127: 128: 129:
130: public function () {
131: $tweetDP = $this->getTweetDataProvider ();
132: if (!$tweetDP) return null;
133: $data = $tweetDP->getData ();
134: $lastTweetId = null;
135: if (count ($data)) {
136: $lastTweetId = $data[count ($data) - 1]['id_str'];
137: }
138: return $lastTweetId;
139: }
140:
141: 142: 143:
144: public function getCacheKey () {
145: $username = $this->_username;
146: return 'TwitterFeedWidget'.$username;
147: }
148:
149: public function run () {
150: $credentials = $this->getTwitterCredentials ();
151: if (!$credentials) return '';
152: if (!extension_loaded('curl')) {
153: $this->addError (Yii::t('app', 'The Twitter widget requires the PHP curl extension.'));
154: return parent::run ();
155: } else {
156: $aliases = $this->getModelTwitterAliases ();
157: if (!count ($aliases)) return '';
158: if (isset ($_GET['twitterScreenName'])) {
159: $this->_username = $_GET['twitterScreenName'];
160: } else {
161: $this->_username = $aliases[0]->alias;
162: }
163: try {
164: $this->getTweetDataProvider ();
165: } catch (TwitterFeedWidgetException $e) {
166: $errorMessage = $e->getMessage ();
167: if (isset ($_GET['twitterFeedAjax'])) {
168: throw new CHttpException (429, $errorMessage);
169: } else {
170: $this->addError ($errorMessage);
171: }
172: }
173: }
174: return parent::run ();
175: }
176:
177: private $_credentials;
178: public function () {
179: if (!isset ($this->_credentials)) {
180: $credId = Yii::app()->settings->twitterCredentialsId;
181: if ($credId && ($credentials = Credentials::model ()->findByPk ($credId))) {
182: $this->_credentials = array(
183: 'oauth_access_token' => $credentials->auth->oauthAccessToken,
184: 'oauth_access_token_secret' => $credentials->auth->oauthAccessTokenSecret,
185: 'consumer_key' => $credentials->auth->consumerKey,
186: 'consumer_secret' => $credentials->auth->consumerSecret,
187: );
188: }
189: }
190: return $this->_credentials;
191: }
192:
193: 194: 195:
196: public function renderTimestamp (array $tweet) {
197: $timestamp = strtotime ($tweet['created_at']);
198: $date = getDate ($timestamp);
199: $nowTs = time ();
200: $now = getDate ($nowTs);
201: $formattedTimestamp = '';
202: if ($date['year'] !== $now['year']) {
203: $formattedTimestamp = Yii::app()->dateFormatter->format(
204: 'd MMM yy', $timestamp);
205: } else if ($date['yday'] !== $now['yday']) {
206: $formattedTimestamp = Yii::app()->dateFormatter->format(
207: 'd MMM', $timestamp);
208: } else if ($now['hours'] - $date['hours'] > 1) {
209: $diffTs = $nowTs - $timestamp;
210: $formattedTimestamp = floor ($diffTs / 60 / 60).'h';
211: } else if ($now['minutes'] - $date['minutes'] > 1) {
212: $diffTs = $nowTs - $timestamp;
213: $formattedTimestamp = floor ($diffTs / 60).'m';
214: } else {
215: $diffTs = $nowTs - $timestamp;
216: $formattedTimestamp = $diffTs.'s';
217: }
218: return '<a href="https://www.twitter.com/'.urlencode ($this->_username).'/status/'.
219: $tweet['id_str'].'">'.
220: CHtml::encode ($formattedTimestamp).
221: '</a>';
222: }
223:
224: 225: 226:
227: public function replaceTextEntities (array &$tweet) {
228: if (isset ($tweet['retweeted_status'])) {
229: $matches = array ();
230: $name = $tweet['user']['name'];
231: $retweetedByText =
232: '<div class="retweeted-by-text-container">
233: <span class="retweet-icon-small"></span>'.
234: CHtml::encode (Yii::t('app', 'Retweeted by')).
235: ' <a href="https://twitter.com/'.urlencode ($name).'">'.
236: $name.
237: '</a>'.
238: '</div>';
239: $tweet = $tweet['retweeted_status'];
240: }
241:
242:
243: if (!isset ($tweet['entities'])) return $tweet['text'];
244: $text = $tweet['text'];
245: $entities = $tweet['entities'];
246: $orderedEntities = array ();
247:
248:
249: foreach ($entities as $type => $entitiesOfType) {
250: foreach ($entitiesOfType as $entity) {
251: $orderedEntities[] = array_merge (array ('type' => $type), $entity);
252: }
253: }
254:
255: usort ($orderedEntities, function ($a, $b) {
256: return $b['indices'][0] - $a['indices'][0];
257: });
258:
259:
260: foreach ($orderedEntities as $entity) {
261: switch ($entity['type']) {
262: case 'hashtags':
263: $link = "<a href='https://twitter.com/hashtag/".
264: urlencode ($entity['text'])."'?src=hash>
265: #".CHtml::encode ($entity['text'])."</a>";
266: break;
267: case 'symbols':
268: $link = "<a href='https://twitter.com/search?1=".
269: urlencode ($entity['text'])."'?src=ctag>
270: $".CHtml::encode ($entity['text'])."</a>";
271: break;
272: case 'urls':
273: $link = "<a
274: title='".$entity['expanded_url']."'
275: href='".$entity['url']."'>
276: ".CHtml::encode ($entity['display_url'])."</a>";
277: break;
278: case 'user_mentions':
279: $link = "<a href='https://twitter.com/".urlencode ($entity['screen_name'])."'>
280: @".CHtml::encode ($entity['screen_name'])."</a>";
281: break;
282: default:
283: continue 2;
284: }
285: $text = mb_substr ($text, 0, $entity['indices'][0], 'UTF-8').$link.
286: mb_substr ($text, $entity['indices'][1] + 1, null, 'UTF-8');
287: }
288:
289: if (isset ($retweetedByText)) $text .= $retweetedByText;
290: $tweet['text'] = $text;
291: }
292:
293: 294: 295: 296:
297: public function remainingRequests ($resourceName, $value=null) {
298: $resourceName = preg_replace ('/\.json$/', '', $resourceName);
299: $rateLimitStatus = $this->getRateLimitStatus ();
300:
301:
302:
303:
304: if (!$rateLimitStatus) {
305: return false;
306: }
307: $matches = array ();
308: preg_match ('/^\/([^\/]+)\//', $resourceName, $matches);
309: $resourceCategory = $matches[1];
310:
311:
312:
313:
314:
315: if (!isset ($rateLimitStatus['resources'][$resourceCategory][$resourceName])) {
316: return false;
317: }
318: if ($value !== null) {
319: $rateLimitStatus['resources'][$resourceCategory][$resourceName][
320: 'remaining'] = $value;
321: $this->setRateLimitStatus ($rateLimitStatus);
322: }
323:
324: $entry = $rateLimitStatus['resources'][$resourceCategory][$resourceName];
325: $remaining = (int) $entry['remaining'];
326:
327:
328:
329: return $remaining;
330: }
331:
332: 333: 334:
335: private function setRateLimitStatus (array $rateLimitStatus) {
336: $this->_rateLimits = $rateLimitStatus;
337: Yii::app()->settings->twitterRateLimits = $rateLimitStatus;
338: Yii::app()->settings->save ();
339: }
340:
341: 342: 343: 344:
345: private $_rateLimits;
346: private function getRateLimitStatus () {
347: if (isset ($this->_rateLimits)) return $this->_rateLimits;
348:
349: $rateLimitWindow = 60 * 15;
350:
351:
352: $rateLimits = Yii::app()->settings->twitterRateLimits;
353: if (ctype_digit ($rateLimits)) {
354: if ((int) $rateLimits >= time ()) {
355: return false;
356: }
357: } elseif (is_array ($rateLimits)) {
358:
359:
360:
361:
362: if (!isset (
363: $rateLimits['resources']['application']['/application/rate_limit_status'])) {
364:
365: Yii::app()->settings->twitterRateLimits = time () + $rateLimitWindow;
366: Yii::app()->settings->save ();
367: return false;
368: }
369:
370: $entry = $rateLimits['resources']['application']['/application/rate_limit_status'];
371: if ($entry['reset'] > time ()) {
372: if ((int) $entry['remaining'] < 1) {
373:
374: return false;
375: } else {
376:
377:
378: return $rateLimits;
379: }
380: }
381:
382: } else if ($rateLimits !== null) {
383:
384: Yii::app()->settings->twitterRateLimits = time () + $rateLimitWindow;
385: Yii::app()->settings->save ();
386: return false;
387: }
388:
389:
390:
391:
392: $credentials = $this->getTwitterCredentials ();
393: $url = 'https://api.twitter.com/1.1/application/rate_limit_status.json';
394: $requestMethod = 'GET';
395: $twitter = new TwitterAPIExchange ($credentials);
396: $rateLimitStatus = CJSON::decode (
397: $twitter
398: ->buildOauth ($url, $requestMethod)
399: ->performRequest ());
400: if (($statusCode = $twitter->getLastStatusCode ()) != 200) {
401: $this->throwApiException ($rateLimitStatus, $statusCode);
402: }
403: Yii::app()->settings->twitterRateLimits = $rateLimitStatus;
404: Yii::app()->settings->save ();
405:
406: $this->_rateLimits = $rateLimitStatus;
407: return $rateLimitStatus;
408: }
409:
410: 411: 412: 413: 414: 415: 416: 417: 418: 419: 420: 421: 422:
423: private ;
424: public function ($append=false) {
425: $this->getRateLimitStatus ();
426: $maxId = isset ($_GET['maxTweetId']) ? $_GET['maxTweetId'] : -1;
427:
428: if (!isset ($this->_tweets) || $append) {
429: $username = $this->_username;
430: $cache = Yii::app()->cache2;
431: $cacheKey = $this->getCacheKey ();
432: $pageSize = 5;
433: $tweets = $cache->get ($cacheKey);
434:
435: if ($append && !$tweets) {
436:
437:
438:
439: $append = false;
440: $maxId = -1;
441: }
442:
443: if (!$tweets || $append) {
444: $tweetCount = 100;
445: $credentials = $this->getTwitterCredentials ();
446: $resourceName = '/statuses/user_timeline.json';
447: $remainingRequests = $this->remainingRequests ($resourceName);
448:
449: if ($remainingRequests < 1) {
450:
451: throw new TwitterFeedWidgetException (Yii::t(
452: 'app', 'Twitter feed could not be retrieved. Please try again later.'));
453: }
454:
455: $url = 'https://api.twitter.com/1.1'.$resourceName;;
456: $getfield = '?screen_name='.$username.'&count='.$tweetCount;
457: if ($append) {
458: $maxId = $tweets[count ($tweets) - 1]['id_str'];
459: $getfield .= '&max_id='.$maxId;
460: }
461:
462: $requestMethod = 'GET';
463: $twitter = new TwitterAPIExchange ($credentials);
464: $oldTweets = $tweets;
465:
466: $tweets = CJSON::decode ($twitter->setGetfield ($getfield)
467: ->buildOauth ($url, $requestMethod)
468: ->performRequest ());
469: if (($statusCode = $twitter->getLastStatusCode ()) != 200) {
470: $this->throwApiException ($tweets, $statusCode);
471: }
472: $this->remainingRequests ($resourceName, $remainingRequests - 1);
473: if ($append) {
474: $tweets = array_merge ($oldTweets, $tweets);
475: }
476: $cache->set ($cacheKey, $tweets, 60 * 5);
477:
478: } else {
479:
480: }
481:
482: if ($maxId === -1) {
483: $this->_tweets = array_slice ($tweets, 0, $pageSize);
484: } else {
485: $tweetCount = count ($tweets);
486: $found = false;
487: for ($i = 0; $i < $tweetCount; $i++) {
488: $tweet = $tweets[$i];
489: if ($tweet['id_str'] == $maxId) {
490: $found = true;
491: break;
492: }
493: }
494: if ($found && $i + $pageSize < $tweetCount) {
495: $this->_tweets = array_slice ($tweets, 0, $i + $pageSize + 1);
496: } else if (!$append) {
497: return $this->requestTweets (true);
498: } else {
499: $this->_tweets = array_slice ($tweets, 0, $pageSize);
500: }
501: }
502: }
503: return $this->_tweets;
504: }
505:
506: private ;
507: public function () {
508: if (!isset ($this->_tweetDataProvider)) {
509: $tweets = $this->requestTweets ();
510: $this->_tweetDataProvider = new CArrayDataProvider ($tweets, array (
511: 'pagination' => array (
512: 'pageSize' => PHP_INT_MAX,
513: ),
514: ));
515: }
516: return $this->_tweetDataProvider;
517: }
518:
519: public function getTimeline () {
520: if (isset ($_GET['twitterFeedAjax'])) {
521: ob_clean ();
522: ob_start ();
523: }
524: $dataProvider = $this->getTweetDataProvider ();
525: if (!$dataProvider) return;
526:
527: Yii::app()->controller->widget ('zii.widgets.CListView', array (
528: 'id' => 'twitter-feed',
529: 'ajaxVar' => 'twitterFeedAjax',
530: 'htmlOptions' => array (
531: 'class' => 'list-view twitter-feed-list-view',
532: ),
533: 'viewData' => array (
534: 'twitterFeedWidget' => $this,
535: ),
536: 'dataProvider' => $dataProvider,
537: 'itemView' => 'application.components.sortableWidget.views._tweet',
538: 'template' => '{items}',
539: ));
540: if (isset ($_GET['twitterFeedAjax'])) {
541: echo '<script>x2.TwitterFeedWidget.lastTweetId = "'.
542: $this->getLastTweetId ().'";</script>';
543: echo ob_get_clean ();
544: ob_flush ();
545: Yii::app()->end ();
546: }
547: }
548:
549: protected function getJSSortableWidgetParams () {
550: if (!isset ($this->_JSSortableWidgetParams)) {
551: if (!$this->hasError ()) {
552: $lastTweetId = $this->getLastTweetId ();
553: } else {
554: $lastTweetId = null;
555: }
556: $this->_JSSortableWidgetParams = array_merge (parent::getJSSortableWidgetParams (),
557: array (
558: 'lastTweetId' => $lastTweetId,
559: )
560: );
561: }
562: return $this->_JSSortableWidgetParams;
563: }
564:
565: 566: 567: 568:
569: private function throwApiException ($response, $code) {
570: $error = isset ($response['error']) ? $response['error'] : '';
571: switch ($code) {
572: case 404:
573: $message = Yii::t('app', 'Twitter username not found.');
574: break;
575: case 401:
576: $message = Yii::t(
577: 'app', 'Twitter Integration credentials are missing or incorrect. Please '.
578: 'contact an administrator.');
579: break;
580: default:
581: $message = Yii::t('app', 'Twitter API {code} error{message}', array (
582: '{code}' => $code,
583: '{message}' => $error ? ': '.$error : '',
584: ));
585: }
586: throw new TwitterFeedWidgetException ($message);
587: }
588: }
589:
590: class extends CException {
591: }
592:
593: ?>
594: