3
* Xibo - Digital Signage - http://www.xibo.org.uk
4
* Copyright (C) 2014-2015 Daniel Garner
6
* This file is part of Xibo.
8
* Xibo is free software: you can redistribute it and/or modify
9
* it under the terms of the GNU Affero General Public License as published by
10
* the Free Software Foundation, either version 3 of the License, or
13
* Xibo is distributed in the hope that it will be useful,
14
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
* GNU Affero General Public License for more details.
18
* You should have received a copy of the GNU Affero General Public License
19
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
22
namespace Xibo\Widget;
26
use Respect\Validation\Validator as v;
27
use Xibo\Exception\ConfigurationException;
28
use Xibo\Factory\ModuleFactory;
31
class TwitterMetro extends ModuleWidget
33
public $codeSchemaVersion = 1;
34
private $resourceFolder;
37
* TwitterMetro constructor.
39
public function init()
41
$this->resourceFolder = PROJECT_ROOT . '/web/modules/twittermetro';
43
// Initialise extra validation rules
44
v::with('Xibo\\Validation\\Rules\\');
48
* Install or Update this module
49
* @param ModuleFactory $moduleFactory
51
public function installOrUpdate($moduleFactory)
53
if ($this->module == null) {
55
$module = $moduleFactory->createEmpty();
56
$module->name = 'Twitter Metro';
57
$module->type = 'twittermetro';
58
$module->class = 'Xibo\Widget\TwitterMetro';
59
$module->description = 'Twitter Metro Search Module';
60
$module->imageUri = 'forms/library.gif';
62
$module->previewEnabled = 1;
63
$module->assignable = 1;
64
$module->regionSpecific = 1;
65
$module->renderAs = 'html';
66
$module->schemaVersion = $this->codeSchemaVersion;
67
$module->defaultDuration = 60;
68
$module->settings = [];
70
$this->setModule($module);
71
$this->installModule();
74
// Check we are all installed
75
$this->installFiles();
81
public function installFiles()
83
$this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/vendor/jquery-1.11.1.min.js')->save();
84
$this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/xibo-metro-render.js')->save();
85
$this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/xibo-layout-scaler.js')->save();
86
$this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/emojione/emojione.sprites.svg')->save();
87
$this->mediaFactory->createModuleSystemFile(PROJECT_ROOT . '/web/modules/vendor/bootstrap.min.css')->save();
89
foreach ($this->mediaFactory->createModuleFileFromFolder($this->resourceFolder) as $media) {
90
/* @var Media $media */
98
public function layoutDesignerJavaScript()
100
// We use the same javascript as the data set view designer
101
return 'twittermetro-form-javascript';
105
* Get the template HTML, CSS, widgetOriginalWidth, widgetOriginalHeight giving its orientation (0:Landscape 1:Portrait)
106
* @param int Orientation
109
public function getTemplateData() {
111
$orientation = ($this->getSanitizer()->getDouble('width', $this->region->width) > $this->getSanitizer()->getDouble('height', $this->region->height)) ? 0 : 1;
113
$templateArray = array(
114
array( 'template' => '<div class="cell-[itemType] [ShadowType] cell" id="item-[itemId]" style="[Photo]"> <div class="item-container [ShadowType]" style="[Color]"> <div class="item-text">[Tweet]</div> <div class="userData"> <div class="tweet-profilePic">[ProfileImage|normal]</div> <div class="tweet-userData"> <div class="user">[User]</div> <small>[Date]</small></div> </div> </div> </div>',
115
'styleSheet' => 'body { font-family: "Helvetica", "Arial", sans-serif; line-height: 1; margin: 0; } #content { width: 1920px !important; height: 1080px !important; background: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 1); } .row-1 { height: 360px; } .page { float: left; margin: 0; padding: 0; } .cell-1 { width: 310px; } .cell-2 { width: 630px; } .cell-3 { width: 950px; } .cell-1, .cell-2, .cell-3 { float: left; height: inherit; margin: 5px; background-repeat: no-repeat; background-size: cover; background-position-x: 50%; background-position-y: 50%; } .item-container { padding: 10px; color: #fff; height: 350px; } .userData { height: 50px; } .darken-container { background-color: rgba(0, 0, 0, 0.4); } .tweet-profilePic { width: 20%; float: left; } .tweet-profilePic img { width: 48px; } .tweet-userData { width: 80%; float: left; text-align: right; } .item-text { padding: 10px; color: #fff; } .emojione { width: 26px; height: 26px; } .cell-1 .item-text { line-height: 30px; font-size: 25px; height: 280px; } .cell-2 .item-text { line-height: 40px; font-size: 40px; height: 280px; } .cell-3 .item-text { line-height: 53px; font-size: 50px; height: 280px; } .user { font-size: 14px; font-weight: bold; padding-top: 20px; } .shadow { text-shadow: 1px 1px 2px rgba(0, 0, 3, 1); } .no-shadow { text-shadow: none !important; } small { font-size: 70%; }',
116
'originalWidth' => '1920',
117
'originalHeight' => '1080'
119
array( 'template' => '<div class="cell-[itemType] [ShadowType] cell" id="item-[itemId]" style="[Photo]"> <div class="item-container [ShadowType]" style="[Color]"> <div class="item-text">[Tweet]</div> <div class="userData"> <div class="tweet-profilePic">[ProfileImage|normal]</div> <div class="tweet-userData"> <div class="user">[User]</div> <small>[Date]</small></div> </div> </div> </div>',
120
'styleSheet' => 'body { font-family: "Helvetica", "Arial", sans-serif; line-height: 1; margin: 0; } #content { width: 1080px !important; height: 1920px !important; background: rgba(255, 255, 255, 0.6); color: rgba(255, 255, 255, 1); } .row-1 { height: 320px; } .page { float: left; margin: 0; padding: 0; } .cell-1 { width: 350px; } .cell-2 { width: 710px; } .cell-3 { width: 1070px; } .cell-1, .cell-2, .cell-3 { float: left; height: inherit; margin: 5px; background-repeat: no-repeat; background-size: cover; background-position-x: 50%; background-position-y: 50%; } .item-container { padding: 10px; color: #fff; height: 310px; } .userData { height: 50px; } .darken-container { background-color: rgba(0, 0, 0, 0.4); } .tweet-profilePic { width: 20%; float: left; } .tweet-profilePic img { width: 48px; } .tweet-userData { width: 80%; float: left; text-align: right; } .item-text { padding: 10px; color: #fff; } .emojione { width: 26px; height: 26px; } .cell-1 .item-text { line-height: 30px; font-size: 25px; height: 240px; } .cell-2 .item-text { line-height: 40px; font-size: 40px; height: 240px; } .cell-3 .item-text { line-height: 53px; font-size: 50px; height: 240px; } .user { font-size: 14px; font-weight: bold; padding-top: 20px; } .shadow { text-shadow: 1px 1px 2px rgba(0, 0, 3, 1); } .no-shadow { text-shadow: none !important; } small { font-size: 70%; }',
121
'originalWidth' => '1080',
122
'originalHeight' => '1920'
126
return $templateArray[$orientation];
130
* Loads color templates for this module
132
private function loadColorTemplates()
134
$this->module->settings['colortemplates'] = [];
136
// Scan the folder for template files
137
foreach (glob(PROJECT_ROOT . '/modules/twittermetro/*.colortemplate.json') as $template) {
138
// Read the contents, json_decode and add to the array
139
$this->module->settings['colortemplates'][] = json_decode(file_get_contents($template), true);
144
* Color templates available
147
public function colorTemplatesAvailable()
149
if (!isset($this->module->settings['colortemplates']))
150
$this->loadColorTemplates();
152
return $this->module->settings['colortemplates'];
156
* Form for updating the module settings
158
public function settingsForm()
160
return 'twitter-form-settings';
164
* Process any module settings
166
public function settings()
168
// Process any module settings you asked for.
169
$apiKey = $this->getSanitizer()->getString('apiKey');
172
throw new \InvalidArgumentException(__('Missing API Key'));
174
// Process any module settings you asked for.
175
$apiSecret = $this->getSanitizer()->getString('apiSecret');
177
if ($apiSecret == '')
178
throw new \InvalidArgumentException(__('Missing API Secret'));
180
$this->module->settings['apiKey'] = $apiKey;
181
$this->module->settings['apiSecret'] = $apiSecret;
182
$this->module->settings['cachePeriod'] = $this->getSanitizer()->getInt('cachePeriod', 300);
183
$this->module->settings['cachePeriodImages'] = $this->getSanitizer()->getInt('cachePeriodImages', 24);
185
// Return an array of the processed settings.
186
return $this->module->settings;
189
public function validate()
191
if ($this->getUseDuration() == 1 && $this->getDuration() == 0)
192
throw new \InvalidArgumentException(__('Please enter a duration'));
194
if (!v::string()->notEmpty()->validate($this->getOption('searchTerm')))
195
throw new \InvalidArgumentException(__('Please enter a search term'));
201
public function add()
203
$this->setCommonOptions();
213
public function edit()
215
$this->setCommonOptions();
223
* Set common options from Request Params
225
private function setCommonOptions()
227
$this->setDuration($this->getSanitizer()->getInt('duration', $this->getDuration()));
228
$this->setUseDuration($this->getSanitizer()->getCheckbox('useDuration'));
229
$this->setOption('name', $this->getSanitizer()->getString('name'));
230
$this->setOption('searchTerm', $this->getSanitizer()->getString('searchTerm'));
231
$this->setOption('effect', $this->getSanitizer()->getString('effect'));
232
$this->setOption('speed', $this->getSanitizer()->getInt('speed'));
233
$this->setOption('backgroundColor', $this->getSanitizer()->getString('backgroundColor'));
234
$this->setOption('noTweetsMessage', $this->getSanitizer()->getString('noTweetsMessage'));
235
$this->setOption('dateFormat', $this->getSanitizer()->getString('dateFormat'));
236
$this->setOption('resultType', $this->getSanitizer()->getString('resultType'));
237
$this->setOption('tweetDistance', $this->getSanitizer()->getInt('tweetDistance'));
238
$this->setOption('tweetCount', $this->getSanitizer()->getInt('tweetCount', 60));
239
$this->setOption('removeUrls', $this->getSanitizer()->getCheckbox('removeUrls'));
240
$this->setOption('removeMentions', $this->getSanitizer()->getCheckbox('removeMentions'));
241
$this->setOption('removeHashtags', $this->getSanitizer()->getCheckbox('removeHashtags'));
242
$this->setOption('overrideColorTemplate', $this->getSanitizer()->getCheckbox('overrideColorTemplate'));
243
$this->setOption('updateInterval', $this->getSanitizer()->getInt('updateInterval', 60));
244
$this->setOption('colorTemplateId', $this->getSanitizer()->getString('colorTemplateId'));
245
$this->setOption('resultContent', $this->getSanitizer()->getString('resultContent'));
246
$this->setOption('removeRetweets', $this->getSanitizer()->getCheckbox('removeRetweets'));
248
// Convert the colors array to string to be able to save it
249
$stringColor = $this->getSanitizer()->getStringArray('color')[0];
250
for ($i=1; $i < count($this->getSanitizer()->getStringArray('color')); $i++) {
251
if(!empty($this->getSanitizer()->getStringArray('color')[$i]))
252
$stringColor .= "|" . $this->getSanitizer()->getStringArray('color')[$i];
254
$this->setOption('templateColours', $stringColor);
257
protected function getToken()
260
$url = 'https://api.twitter.com/oauth2/token';
262
// Prepare the consumer key and secret
263
$key = base64_encode(urlencode($this->getSetting('apiKey')) . ':' . urlencode($this->getSetting('apiSecret')));
265
// Check to see if we have the bearer token already cached
266
$cache = $this->getPool()->getItem('bearer_' . $key);
268
$token = $cache->get();
270
if ($cache->isHit()) {
271
$this->getLog()->debug('Bearer Token served from cache');
275
$this->getLog()->debug('Bearer Token served from API');
277
// Shame - we will need to get it.
279
$httpOptions = array(
280
CURLOPT_TIMEOUT => 20,
281
CURLOPT_SSL_VERIFYPEER => true,
282
CURLOPT_HTTPHEADER => array(
283
'POST /oauth2/token HTTP/1.1',
284
'Authorization: Basic ' . $key,
285
'Content-Type: application/x-www-form-urlencoded;charset=UTF-8',
288
CURLOPT_USERAGENT => 'Xibo Twitter Metro Module',
289
CURLOPT_HEADER => false,
290
CURLINFO_HEADER_OUT => true,
291
CURLOPT_RETURNTRANSFER => true,
292
CURLOPT_POST => true,
293
CURLOPT_POSTFIELDS => http_build_query(array('grant_type' => 'client_credentials')),
298
if ($this->getConfig()->GetSetting('PROXY_HOST') != '' && !$this->getConfig()->isProxyException($url)) {
299
$httpOptions[CURLOPT_PROXY] = $this->getConfig()->GetSetting('PROXY_HOST');
300
$httpOptions[CURLOPT_PROXYPORT] = $this->getConfig()->GetSetting('PROXY_PORT');
302
if ($this->getConfig()->GetSetting('PROXY_AUTH') != '')
303
$httpOptions[CURLOPT_PROXYUSERPWD] = $this->getConfig()->GetSetting('PROXY_AUTH');
309
curl_setopt_array($curl, $httpOptions);
312
if (!$result = curl_exec($curl)) {
314
$this->getLog()->error('Error contacting Twitter API: ' . curl_error($curl));
318
// We want to check for a 200
319
$outHeaders = curl_getinfo($curl);
321
if ($outHeaders['http_code'] != 200) {
322
$this->getLog()->error('Twitter API returned ' . $result . ' status. Unable to proceed. Headers = ' . var_export($outHeaders, true));
324
// See if we can parse the error.
325
$body = json_decode($result);
327
$this->getLog()->error('Twitter Error: ' . ((isset($body->errors[0])) ? $body->errors[0]->message : 'Unknown Error'));
332
// See if we can parse the body as JSON.
333
$body = json_decode($result);
335
// We have a 200 - therefore we want to think about caching the bearer token
336
// First, lets check its a bearer token
337
if ($body->token_type != 'bearer') {
338
$this->getLog()->error('Twitter API returned OK, but without a bearer token. ' . var_export($body, true));
342
// It is, so lets cache it
344
$cache->set($body->access_token);
345
$cache->expiresAfter(100000);
346
$this->getPool()->saveDeferred($cache);
348
return $body->access_token;
351
protected function searchApi($token, $term, $resultType = 'mixed', $geoCode = '', $count = 18)
354
// Construct the URL to call
355
$url = 'https://api.twitter.com/1.1/search/tweets.json';
356
$queryString = '?q=' . urlencode(trim($term)) .
357
'&result_type=' . $resultType .
359
'&include_entities=true' .
360
'&tweet_mode=extended';
363
$queryString .= '&geocode=' . $geoCode;
365
$httpOptions = array(
366
CURLOPT_TIMEOUT => 20,
367
CURLOPT_SSL_VERIFYPEER => true,
368
CURLOPT_HTTPHEADER => array(
369
'GET /1.1/search/tweets.json' . $queryString . 'HTTP/1.1',
370
'Host: api.twitter.com',
371
'Authorization: Bearer ' . $token
373
CURLOPT_USERAGENT => 'Xibo Twitter Module',
374
CURLOPT_HEADER => false,
375
CURLINFO_HEADER_OUT => true,
376
CURLOPT_RETURNTRANSFER => true,
377
CURLOPT_URL => $url . $queryString,
381
if ($this->getConfig()->GetSetting('PROXY_HOST') != '' && !$this->getConfig()->isProxyException($url)) {
382
$httpOptions[CURLOPT_PROXY] = $this->getConfig()->GetSetting('PROXY_HOST');
383
$httpOptions[CURLOPT_PROXYPORT] = $this->getConfig()->GetSetting('PROXY_PORT');
385
if ($this->getConfig()->GetSetting('PROXY_AUTH') != '')
386
$httpOptions[CURLOPT_PROXYUSERPWD] = $this->getConfig()->GetSetting('PROXY_AUTH');
389
$this->getLog()->debug('Calling API with: ' . $url . $queryString);
392
curl_setopt_array($curl, $httpOptions);
393
$result = curl_exec($curl);
395
// Get the response headers
396
$outHeaders = curl_getinfo($curl);
398
if ($outHeaders['http_code'] == 0) {
400
$this->getLog()->error('Unable to reach twitter api.');
402
} else if ($outHeaders['http_code'] != 200) {
403
$this->getLog()->error('Twitter API returned ' . $outHeaders['http_code'] . ' status. Unable to proceed. Headers = ' . var_export($outHeaders, true));
405
// See if we can parse the error.
406
$body = json_decode($result);
408
$this->getLog()->error('Twitter Error: ' . ((isset($body->errors[0])) ? $body->errors[0]->message : 'Unknown Error'));
413
// Parse out header and body
414
$body = json_decode($result);
419
protected function getTwitterFeed($displayId = 0, $isPreview = true)
421
if (!extension_loaded('curl'))
422
throw new ConfigurationException(__('cURL extension is required for Twitter'));
424
// Do we need to add a geoCode?
426
$distance = $this->getOption('tweetDistance');
427
if ($distance != 0) {
428
// Use the display ID or the default.
429
if ($displayId != 0) {
430
// Look up the lat/long
431
$display = $this->displayFactory->getById($displayId);
432
$defaultLat = $display->latitude;
433
$defaultLong = $display->longitude;
435
$defaultLat = $this->getConfig()->GetSetting('DEFAULT_LAT');
436
$defaultLong = $this->getConfig()->GetSetting('DEFAULT_LONG');
439
// Built the geoCode string.
440
$geoCode = implode(',', array($defaultLat, $defaultLong, $distance)) . 'mi';
443
// Search content filtered by type of tweets
444
$searchTerm = $this->getOption('searchTerm');
445
$resultContent = $this->getOption('resultContent');
447
switch ($resultContent) {
455
$searchTerm .= ' -filter:media';
459
// Only tweets with native images
460
$searchTerm .= ' filter:twimg';
468
// Search term retweets filter
469
$searchTerm .= ($this->getOption('removeRetweets')) ? ' -filter:retweets' : '';
471
// Connect to twitter and get the twitter feed.
472
$cache = $this->getPool()->getItem(md5($searchTerm . $this->getOption('resultType') . $this->getOption('tweetCount', 60) . $geoCode));
474
$data = $cache->get();
476
if ($cache->isMiss()) {
478
$this->getLog()->debug('Querying API for ' . $searchTerm);
480
// We need to search for it
481
if (!$token = $this->getToken())
484
// We have the token, make a tweet
485
if (!$data = $this->searchApi($token, $searchTerm, $this->getOption('resultType'), $geoCode, $this->getOption('tweetCount', 60)))
490
$cache->expiresAfter($this->getSetting('cachePeriod', 3600));
491
$this->getPool()->saveDeferred($cache);
494
// Get the template data
495
$templateData = $this->getTemplateData();
496
$template = $this->parseLibraryReferences($isPreview, $templateData['template']);
498
// Parse the text template
500
preg_match_all('/\[.*?\]/', $template, $matches);
502
// Build an array to return
505
// Expiry time for any media that is downloaded
506
$expires = $this->getDate()->parse()->addHours($this->getSetting('cachePeriodImages', 24))->format('U');
508
// Remove URL setting
509
$removeUrls = $this->getOption('removeUrls', 1) == 1;
510
$removeMentions = $this->getOption('removeMentions', 1) == 1;
511
$removeHashTags = $this->getOption('removeHashTags', 1) == 1;
513
// If we have nothing to show, display a no tweets message.
514
if (count($data->statuses) <= 0) {
515
// Create ourselves an empty tweet so that the rest of the code can continue as normal
516
$user = new \stdClass();
518
$user->screen_name = '';
519
$user->profile_image_url = '';
520
$user->location = '';
522
$tweet = new \stdClass();
523
$tweet->full_text = $this->getOption('noTweetsMessage', __('There are no tweets to display'));
524
$tweet->created_at = '';
525
$tweet->user = $user;
527
// Append to our statuses
528
$data->statuses[] = $tweet;
531
// Make an emojione client
532
$emoji = new Client(new Ruleset());
533
$emoji->imageType = 'svg';
534
$emoji->sprites = true;
535
$emoji->imagePathSVGSprites = $this->getResourceUrl('emojione/emojione.sprites.svg');
537
// Get the date format to apply
538
$dateFormat = $this->getOption('dateFormat', $this->getConfig()->GetSetting('DATE_FORMAT'));
541
// This should return the formatted items.
542
foreach ($data->statuses as $tweet) {
543
// Substitute for all matches in the template
544
$rowString = $template;
546
foreach ($matches[0] as $sub) {
547
// Always clear the stored template replacement
549
$tagOptions = array();
551
// Get the options from the tag and create an array
552
$subClean = str_replace('[', '', str_replace(']', '', $sub));
553
if (stripos($subClean, '|') > -1) {
554
$tagOptions = explode('|', $subClean);
557
$subClean = $tagOptions[0];
559
// Remove the tag from the first position
560
array_shift($tagOptions);
563
// Maybe make this more generic?
566
// Get the tweet text to operate on
567
$tweetText = $tweet->full_text;
569
// Replace URLs with their display_url before removal
570
if (isset($tweet->entities->urls)) {
571
foreach ($tweet->entities->urls as $url) {
572
$tweetText = str_replace($url->url, $url->display_url, $tweetText);
576
// Clean up the tweet text
577
// thanks to https://github.com/solarbug (https://github.com/xibosignage/xibo/issues/703)
580
$tweetText = preg_replace('/(\s+|^)@\S+/', '', $tweetText);
584
$tweetText = preg_replace('/(\s+|^)#\S+/', '', $tweetText);
587
// Regex taken from http://daringfireball.net/2010/07/improved_regex_for_matching_urls
588
$tweetText = preg_replace('~(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))~', '', $tweetText); // remove urls
590
$replace = $emoji->toImage($tweetText);
594
$replace = $tweet->user->name;
598
$replace = ($tweet->user->screen_name != '') ? ('@' . $tweet->user->screen_name) : '';
602
if($tweet->created_at != '')
603
$replace = $this->getDate()->getLocalDate(strtotime($tweet->created_at), $dateFormat);
607
$replace = $tweet->user->location;
611
// Grab the profile image
612
if ($tweet->user->profile_image_url != '') {
614
// Original Default Image
616
if( count($tagOptions) > 0 ) {
617
// Image options ( normal, bigger, mini )
618
$imageSizeType = '_' . $tagOptions[0];
621
// Twitter image size
622
$tweet->user->profile_image_url = str_replace('_normal', $imageSizeType, $tweet->user->profile_image_url);
624
// Grab the profile image
625
$file = $this->mediaFactory->createModuleFile('twitter_' . $tweet->user->id, $tweet->user->profile_image_url);
626
$file->isRemote = true;
627
$file->expires = $expires;
630
// Tag this layout with this file
631
$this->assignMedia($file->mediaId);
633
$replace = ($isPreview)
634
? '<img src="' . $this->getApp()->urlFor('library.download', ['id' => $file->mediaId, 'type' => 'image']) . '?preview=1" />'
635
: '<img src="' . $file->storedAs . '" />';
640
// See if there is a profile image
641
if (!$this->tweetHasPhoto($tweet)) {
643
// Get the colors array
644
$colorArray = explode("|", $this->getOption('templateColours'));
646
// Find a random color
647
$randomNum = rand(0,count($colorArray)-1);
648
$randomColor = $colorArray[$randomNum];
650
$replace = 'background-color:' . $randomColor;
655
// See if there is a profile image
656
$replace = ($this->tweetHasPhoto($tweet)) ? 'shadow darken-container' : '';
660
// See if there are any photos associated with this tweet.
661
if ($this->tweetHasPhoto($tweet)) {
663
// See if it's an image from a tweet or RT, and only take the first one
664
$mediaObject = (isset($tweet->entities->media))
665
? $tweet->entities->media[0]
666
: $tweet->retweeted_status->entities->media[0];
668
$photoUrl = $mediaObject->media_url;
670
if ($photoUrl != '') {
671
$file = $this->mediaFactory->createModuleFile('twitter_photo_' . $tweet->user->id . '_' . $mediaObject->id_str, $photoUrl);
672
$file->isRemote = true;
673
$file->expires = $expires;
676
// Tag this layout with this file
677
$this->assignMedia($file->mediaId);
679
$replace = "background-image: url("
680
. (($isPreview) ? $this->getApp()->urlFor('library.download', ['id' => $file->mediaId, 'type' => 'image']) : $file->storedAs)
686
case 'TwitterLogoWhite':
687
//Get the Twitter logo image file path
688
$replace = $this->getResourceUrl('twitter/twitter_white.png');
691
case 'TwitterLogoBlue':
692
//Get the Twitter logo image file path
693
$replace = $this->getResourceUrl('twitter/twitter_blue.png');
697
$replace = '[' . $subClean . ']';
700
$rowString = str_replace($sub, $replace, $rowString);
703
// Substitute the replacement we have found (it might be '')
704
$return[] = $rowString;
707
// Return the data array
713
* @param int $displayId
716
public function getResource($displayId = 0)
718
// Make sure we are set up correctly
719
if ($this->getSetting('apiKey') == '' || $this->getSetting('apiSecret') == '') {
720
$this->getLog()->error('Twitter Module not configured. Missing API Keys');
725
$isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
727
// Replace the View Port Width?
728
$data['viewPortWidth'] = ($isPreview) ? $this->region->width : '[[ViewPortWidth]]';
730
// Information from the Module
731
$duration = $this->getCalculatedDurationForGetResource();
733
// Generate a JSON string of substituted items.
734
$items = $this->getTwitterFeed($displayId, $isPreview);
736
// Get the template data
737
$templateData = $this->getTemplateData();
739
// Return empty string if there are no items to show.
740
if (count($items) == 0)
744
'type' => $this->getModuleType(),
745
'fx' => $this->getOption('effect', 'noAnim'),
746
'speed' => $this->getOption('speed', 500),
747
'duration' => $duration,
748
'numItems' => count($items),
749
'originalWidth' => $this->region->width,
750
'originalHeight' => $this->region->height,
751
'previewWidth' => $this->getSanitizer()->getDouble('width', 0),
752
'previewHeight' => $this->getSanitizer()->getDouble('height', 0),
753
'scaleOverride' => $this->getSanitizer()->getDouble('scale_override', 0),
754
'widgetDesignWidth' => $templateData['originalWidth'],
755
'widgetDesignHeight'=> $templateData['originalHeight'],
756
'resultContent'=> $this->getSanitizer()->string($this->getOption('resultContent'))
759
// Replace the control meta with our data from twitter
760
$data['controlMeta'] = '<!-- NUMITEMS=' . count($items) . ' -->' . PHP_EOL . '<!-- DURATION=' . $duration . ' -->';
762
// Replace the head content
765
$backgroundColor = $this->getOption('backgroundColor');
766
if ($backgroundColor != '') {
767
$headContent .= '<style type="text/css">body, .page, .item { background-color: ' . $backgroundColor . ' }</style>';
770
// Add our fonts.css file
771
$headContent .= '<link href="' . (($isPreview) ? $this->getApp()->urlFor('library.font.css') : 'fonts.css') . '" rel="stylesheet" media="screen">
772
<link href="' . $this->getResourceUrl('vendor/bootstrap.min.css') . '" rel="stylesheet" media="screen">';
774
// Add the CSS if it isn't empty
775
$css = $templateData['styleSheet'];
778
$headContent .= '<style type="text/css">' . $this->parseLibraryReferences($isPreview, $css) . '</style>';
780
$headContent .= '<style type="text/css">' . file_get_contents($this->getConfig()->uri('css/client.css', true)) . '</style>';
782
// Replace the Head Content with our generated javascript
783
$data['head'] = $headContent;
785
// Add some scripts to the JavaScript Content
786
$javaScriptContent = '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery-1.11.1.min.js') . '"></script>';
788
// Get the colors array
789
$colorArray = explode("|", $this->getOption('templateColours'));
791
// Need the cycle plugin?
792
if ($this->getOption('effect') != 'none')
793
$javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('vendor/jquery-cycle-2.1.6.min.js') . '"></script>';
795
$javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-layout-scaler.js') . '"></script>';
796
$javaScriptContent .= '<script type="text/javascript" src="' . $this->getResourceUrl('xibo-metro-render.js') . '"></script>';
798
$javaScriptContent .= '<script type="text/javascript">';
799
$javaScriptContent .= ' var options = ' . json_encode($options) . ';';
800
$javaScriptContent .= ' var items = ' . json_encode($items) . ';';
801
$javaScriptContent .= ' var colors = ' . json_encode($colorArray) . ';';
802
$javaScriptContent .= ' $(document).ready(function() { ';
803
$javaScriptContent .= ' $("body").xiboLayoutScaler(options); $("#content").xiboMetroRender(options, items, colors); ';
804
$javaScriptContent .= ' }); ';
805
$javaScriptContent .= '</script>';
807
// Replace the Head Content with our generated javascript
808
$data['javaScript'] = $javaScriptContent;
810
// Update and save widget if we've changed our assignments.
811
if ($this->hasMediaChanged())
812
$this->widget->save(['saveWidgetOptions' => false, 'notifyDisplays' => true]);
814
return $this->renderTemplate($data);
817
public function isValid()
819
// Using the information you have in your module calculate whether it is valid or not.
826
public function tweetHasPhoto($tweet)
828
return ((isset($tweet->entities->media) && count($tweet->entities->media) > 0) || (isset($tweet->retweeted_status->entities->media) && count($tweet->retweeted_status->entities->media) > 0));