~xibo-maintainers/xibo/tempel

« back to all changes in this revision

Viewing changes to lib/Widget/TwitterMetro.php

  • Committer: GitHub
  • Author(s): Dan Garner
  • Date: 2016-11-01 12:15:55 UTC
  • Revision ID: git-v1:3a57269f25d6fec7c48eae025f8c1025acc2dafc
Twitter Metro Module (#224)

* Metro Twitter development: first version xibosignage/xibo#927

* Metro Twitter development: forms update, dynamic layout xibosignage/xibo#927

* Metro Twitter development: retweets filter and some fixes xibosignage/xibo#927

* Metro Twitter development: IE compatibility fix xibosignage/xibo#927

* Metro Twitter development: Effect cycle adjustment xibosignage/xibo#927

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
/*
 
3
 * Xibo - Digital Signage - http://www.xibo.org.uk
 
4
 * Copyright (C) 2014-2015 Daniel Garner
 
5
 *
 
6
 * This file is part of Xibo.
 
7
 *
 
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
 
11
 * any later version. 
 
12
 *
 
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.
 
17
 *
 
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/>.
 
20
 *
 
21
 */
 
22
namespace Xibo\Widget;
 
23
 
 
24
use Emojione\Client;
 
25
use Emojione\Ruleset;
 
26
use Respect\Validation\Validator as v;
 
27
use Xibo\Exception\ConfigurationException;
 
28
use Xibo\Factory\ModuleFactory;
 
29
 
 
30
 
 
31
class TwitterMetro extends ModuleWidget
 
32
{
 
33
    public $codeSchemaVersion = 1;
 
34
    private $resourceFolder;
 
35
 
 
36
    /**
 
37
     * TwitterMetro constructor.
 
38
     */
 
39
    public function init()
 
40
    {
 
41
        $this->resourceFolder = PROJECT_ROOT . '/web/modules/twittermetro';
 
42
 
 
43
        // Initialise extra validation rules
 
44
        v::with('Xibo\\Validation\\Rules\\');
 
45
    }
 
46
    
 
47
    /**
 
48
     * Install or Update this module
 
49
     * @param ModuleFactory $moduleFactory
 
50
     */
 
51
    public function installOrUpdate($moduleFactory)
 
52
    {
 
53
        if ($this->module == null) {
 
54
            // Install
 
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';
 
61
            $module->enabled = 1;
 
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 = [];
 
69
 
 
70
            $this->setModule($module);
 
71
            $this->installModule();
 
72
        }
 
73
 
 
74
        // Check we are all installed
 
75
        $this->installFiles();
 
76
    }
 
77
 
 
78
    /**
 
79
     * Install Files
 
80
     */
 
81
    public function installFiles()
 
82
    {
 
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();
 
88
        
 
89
        foreach ($this->mediaFactory->createModuleFileFromFolder($this->resourceFolder) as $media) {
 
90
            /* @var Media $media */
 
91
            $media->save();
 
92
        }
 
93
    }
 
94
    
 
95
    /**
 
96
     * @return string
 
97
     */
 
98
    public function layoutDesignerJavaScript()
 
99
    {
 
100
        // We use the same javascript as the data set view designer
 
101
        return 'twittermetro-form-javascript';
 
102
    }
 
103
    
 
104
    /**
 
105
     * Get the template HTML, CSS, widgetOriginalWidth, widgetOriginalHeight giving its orientation (0:Landscape 1:Portrait)
 
106
     * @param int Orientation
 
107
     * @return array
 
108
     */
 
109
    public function getTemplateData() {
 
110
        
 
111
        $orientation = ($this->getSanitizer()->getDouble('width', $this->region->width) > $this->getSanitizer()->getDouble('height', $this->region->height)) ? 0 : 1; 
 
112
        
 
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'
 
118
            ),
 
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'
 
123
            )
 
124
        );
 
125
        
 
126
        return $templateArray[$orientation];
 
127
    }
 
128
    
 
129
    /**
 
130
    * Loads color templates for this module
 
131
    */
 
132
    private function loadColorTemplates()
 
133
    {
 
134
        $this->module->settings['colortemplates'] = [];
 
135
 
 
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);
 
140
        }
 
141
    }
 
142
 
 
143
    /**
 
144
    * Color templates available
 
145
    * @return array
 
146
    */
 
147
    public function colorTemplatesAvailable()
 
148
    {
 
149
        if (!isset($this->module->settings['colortemplates']))
 
150
            $this->loadColorTemplates();
 
151
 
 
152
        return $this->module->settings['colortemplates'];
 
153
    }
 
154
 
 
155
    /**
 
156
     * Form for updating the module settings
 
157
     */
 
158
    public function settingsForm()
 
159
    {
 
160
        return 'twitter-form-settings';
 
161
    }
 
162
 
 
163
    /**
 
164
     * Process any module settings
 
165
     */
 
166
    public function settings()
 
167
    {
 
168
        // Process any module settings you asked for.
 
169
        $apiKey = $this->getSanitizer()->getString('apiKey');
 
170
 
 
171
        if ($apiKey == '')
 
172
            throw new \InvalidArgumentException(__('Missing API Key'));
 
173
 
 
174
        // Process any module settings you asked for.
 
175
        $apiSecret = $this->getSanitizer()->getString('apiSecret');
 
176
 
 
177
        if ($apiSecret == '')
 
178
            throw new \InvalidArgumentException(__('Missing API Secret'));
 
179
 
 
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);
 
184
 
 
185
        // Return an array of the processed settings.
 
186
        return $this->module->settings;
 
187
    }
 
188
 
 
189
    public function validate()
 
190
    {
 
191
        if ($this->getUseDuration() == 1 && $this->getDuration() == 0)
 
192
            throw new \InvalidArgumentException(__('Please enter a duration'));
 
193
 
 
194
        if (!v::string()->notEmpty()->validate($this->getOption('searchTerm')))
 
195
            throw new \InvalidArgumentException(__('Please enter a search term'));
 
196
    }
 
197
 
 
198
    /**
 
199
     * Add Media
 
200
     */
 
201
    public function add()
 
202
    {
 
203
        $this->setCommonOptions();
 
204
 
 
205
        // Save the widget
 
206
        $this->validate();
 
207
        $this->saveWidget();
 
208
    }
 
209
 
 
210
    /**
 
211
     * Edit Media
 
212
     */
 
213
    public function edit()
 
214
    {
 
215
        $this->setCommonOptions();
 
216
 
 
217
        // Save the widget
 
218
        $this->validate();
 
219
        $this->saveWidget();
 
220
    }
 
221
 
 
222
    /**
 
223
     * Set common options from Request Params
 
224
     */
 
225
    private function setCommonOptions()
 
226
    {
 
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'));
 
247
        
 
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];
 
253
        }
 
254
        $this->setOption('templateColours', $stringColor);
 
255
    }
 
256
 
 
257
    protected function getToken()
 
258
    {
 
259
        // Prepare the URL
 
260
        $url = 'https://api.twitter.com/oauth2/token';
 
261
 
 
262
        // Prepare the consumer key and secret
 
263
        $key = base64_encode(urlencode($this->getSetting('apiKey')) . ':' . urlencode($this->getSetting('apiSecret')));
 
264
 
 
265
        // Check to see if we have the bearer token already cached
 
266
        $cache = $this->getPool()->getItem('bearer_' . $key);
 
267
 
 
268
        $token = $cache->get();
 
269
 
 
270
        if ($cache->isHit()) {
 
271
            $this->getLog()->debug('Bearer Token served from cache');
 
272
            return $token;
 
273
        }
 
274
 
 
275
        $this->getLog()->debug('Bearer Token served from API');
 
276
 
 
277
        // Shame - we will need to get it.
 
278
        // and store 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',
 
286
                'Content-Length: 29'
 
287
            ),
 
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')),
 
294
            CURLOPT_URL => $url,
 
295
        );
 
296
 
 
297
        // Proxy support
 
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');
 
301
 
 
302
            if ($this->getConfig()->GetSetting('PROXY_AUTH') != '')
 
303
                $httpOptions[CURLOPT_PROXYUSERPWD] = $this->getConfig()->GetSetting('PROXY_AUTH');
 
304
        }
 
305
 
 
306
        $curl = curl_init();
 
307
 
 
308
        // Set options
 
309
        curl_setopt_array($curl, $httpOptions);
 
310
 
 
311
        // Call exec
 
312
        if (!$result = curl_exec($curl)) {
 
313
            // Log the error
 
314
            $this->getLog()->error('Error contacting Twitter API: ' . curl_error($curl));
 
315
            return false;
 
316
        }
 
317
 
 
318
        // We want to check for a 200
 
319
        $outHeaders = curl_getinfo($curl);
 
320
 
 
321
        if ($outHeaders['http_code'] != 200) {
 
322
            $this->getLog()->error('Twitter API returned ' . $result . ' status. Unable to proceed. Headers = ' . var_export($outHeaders, true));
 
323
 
 
324
            // See if we can parse the error.
 
325
            $body = json_decode($result);
 
326
 
 
327
            $this->getLog()->error('Twitter Error: ' . ((isset($body->errors[0])) ? $body->errors[0]->message : 'Unknown Error'));
 
328
 
 
329
            return false;
 
330
        }
 
331
 
 
332
        // See if we can parse the body as JSON.
 
333
        $body = json_decode($result);
 
334
 
 
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));
 
339
            return false;
 
340
        }
 
341
 
 
342
        // It is, so lets cache it
 
343
        // long times...
 
344
        $cache->set($body->access_token);
 
345
        $cache->expiresAfter(100000);
 
346
        $this->getPool()->saveDeferred($cache);
 
347
 
 
348
        return $body->access_token;
 
349
    }
 
350
 
 
351
    protected function searchApi($token, $term, $resultType = 'mixed', $geoCode = '', $count = 18)
 
352
    {
 
353
        
 
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 .
 
358
            '&count=' . $count .
 
359
            '&include_entities=true' . 
 
360
            '&tweet_mode=extended';
 
361
 
 
362
        if ($geoCode != '')
 
363
            $queryString .= '&geocode=' . $geoCode;
 
364
 
 
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
 
372
            ),
 
373
            CURLOPT_USERAGENT => 'Xibo Twitter Module',
 
374
            CURLOPT_HEADER => false,
 
375
            CURLINFO_HEADER_OUT => true,
 
376
            CURLOPT_RETURNTRANSFER => true,
 
377
            CURLOPT_URL => $url . $queryString,
 
378
        );
 
379
 
 
380
        // Proxy support
 
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');
 
384
 
 
385
            if ($this->getConfig()->GetSetting('PROXY_AUTH') != '')
 
386
                $httpOptions[CURLOPT_PROXYUSERPWD] = $this->getConfig()->GetSetting('PROXY_AUTH');
 
387
        }
 
388
 
 
389
        $this->getLog()->debug('Calling API with: ' . $url . $queryString);
 
390
 
 
391
        $curl = curl_init();
 
392
        curl_setopt_array($curl, $httpOptions);
 
393
        $result = curl_exec($curl);
 
394
 
 
395
        // Get the response headers
 
396
        $outHeaders = curl_getinfo($curl);
 
397
 
 
398
        if ($outHeaders['http_code'] == 0) {
 
399
            // Unable to connect
 
400
            $this->getLog()->error('Unable to reach twitter api.');
 
401
            return false;
 
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));
 
404
 
 
405
            // See if we can parse the error.
 
406
            $body = json_decode($result);
 
407
 
 
408
            $this->getLog()->error('Twitter Error: ' . ((isset($body->errors[0])) ? $body->errors[0]->message : 'Unknown Error'));
 
409
 
 
410
            return false;
 
411
        }
 
412
 
 
413
        // Parse out header and body
 
414
        $body = json_decode($result);
 
415
 
 
416
        return $body;
 
417
    }
 
418
 
 
419
    protected function getTwitterFeed($displayId = 0, $isPreview = true)
 
420
    {
 
421
        if (!extension_loaded('curl'))
 
422
            throw new ConfigurationException(__('cURL extension is required for Twitter'));
 
423
 
 
424
        // Do we need to add a geoCode?
 
425
        $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;
 
434
            } else {
 
435
                $defaultLat = $this->getConfig()->GetSetting('DEFAULT_LAT');
 
436
                $defaultLong = $this->getConfig()->GetSetting('DEFAULT_LONG');
 
437
            }
 
438
 
 
439
            // Built the geoCode string.
 
440
            $geoCode = implode(',', array($defaultLat, $defaultLong, $distance)) . 'mi';
 
441
        }
 
442
        
 
443
        // Search content filtered by type of tweets  
 
444
        $searchTerm = $this->getOption('searchTerm');
 
445
        $resultContent = $this->getOption('resultContent');
 
446
        
 
447
        switch ($resultContent) {
 
448
          case 0:
 
449
            //Default
 
450
            $searchTerm .= '';
 
451
            break;
 
452
            
 
453
          case 1:
 
454
            // Remove media
 
455
            $searchTerm .= ' -filter:media';
 
456
            break;
 
457
            
 
458
          case 2:
 
459
            // Only tweets with native images
 
460
            $searchTerm .= ' filter:twimg';
 
461
            break; 
 
462
               
 
463
          default:
 
464
            $searchTerm .= '';
 
465
            break;
 
466
        }
 
467
        
 
468
        // Search term retweets filter
 
469
        $searchTerm .= ($this->getOption('removeRetweets')) ? ' -filter:retweets' : '';
 
470
        
 
471
        // Connect to twitter and get the twitter feed.
 
472
        $cache = $this->getPool()->getItem(md5($searchTerm . $this->getOption('resultType') . $this->getOption('tweetCount', 60) . $geoCode));
 
473
 
 
474
        $data = $cache->get();
 
475
 
 
476
        if ($cache->isMiss()) {
 
477
 
 
478
            $this->getLog()->debug('Querying API for ' . $searchTerm);
 
479
 
 
480
            // We need to search for it
 
481
            if (!$token = $this->getToken())
 
482
                return false;
 
483
 
 
484
            // We have the token, make a tweet
 
485
            if (!$data = $this->searchApi($token, $searchTerm, $this->getOption('resultType'), $geoCode, $this->getOption('tweetCount', 60)))
 
486
                return false;
 
487
 
 
488
            // Cache it
 
489
            $cache->set($data);
 
490
            $cache->expiresAfter($this->getSetting('cachePeriod', 3600));
 
491
            $this->getPool()->saveDeferred($cache);
 
492
        }
 
493
 
 
494
        // Get the template data
 
495
        $templateData = $this->getTemplateData();
 
496
        $template = $this->parseLibraryReferences($isPreview, $templateData['template']);
 
497
 
 
498
        // Parse the text template
 
499
        $matches = '';
 
500
        preg_match_all('/\[.*?\]/', $template, $matches);
 
501
 
 
502
        // Build an array to return
 
503
        $return = array();
 
504
 
 
505
        // Expiry time for any media that is downloaded
 
506
        $expires = $this->getDate()->parse()->addHours($this->getSetting('cachePeriodImages', 24))->format('U');
 
507
 
 
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;
 
512
 
 
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();
 
517
            $user->name = '';
 
518
            $user->screen_name = '';
 
519
            $user->profile_image_url = '';
 
520
            $user->location = '';
 
521
 
 
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;
 
526
 
 
527
            // Append to our statuses
 
528
            $data->statuses[] = $tweet;
 
529
        }
 
530
 
 
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');
 
536
 
 
537
        // Get the date format to apply
 
538
        $dateFormat = $this->getOption('dateFormat', $this->getConfig()->GetSetting('DATE_FORMAT'));
 
539
        
 
540
 
 
541
        // This should return the formatted items.
 
542
        foreach ($data->statuses as $tweet) {
 
543
            // Substitute for all matches in the template
 
544
            $rowString = $template;
 
545
 
 
546
            foreach ($matches[0] as $sub) {
 
547
                // Always clear the stored template replacement
 
548
                $replace = '';
 
549
                $tagOptions = array();
 
550
                
 
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);
 
555
                    
 
556
                    // Save the main tag 
 
557
                    $subClean = $tagOptions[0];
 
558
                    
 
559
                    // Remove the tag from the first position
 
560
                    array_shift($tagOptions);
 
561
                }
 
562
                
 
563
                // Maybe make this more generic?
 
564
                switch ($subClean) {
 
565
                    case 'Tweet':
 
566
                        // Get the tweet text to operate on
 
567
                        $tweetText = $tweet->full_text;
 
568
 
 
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);
 
573
                            }
 
574
                        }
 
575
 
 
576
                        // Clean up the tweet text
 
577
                        // thanks to https://github.com/solarbug (https://github.com/xibosignage/xibo/issues/703)
 
578
                        // Remove Mentions
 
579
                        if ($removeMentions)
 
580
                            $tweetText = preg_replace('/(\s+|^)@\S+/', '', $tweetText);
 
581
 
 
582
                        // Remove HashTags
 
583
                        if ($removeHashTags)
 
584
                            $tweetText = preg_replace('/(\s+|^)#\S+/', '', $tweetText);
 
585
 
 
586
                        if ($removeUrls)
 
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
 
589
 
 
590
                        $replace = $emoji->toImage($tweetText);
 
591
                        break;
 
592
 
 
593
                    case 'User':
 
594
                        $replace = $tweet->user->name;
 
595
                        break;
 
596
 
 
597
                    case 'ScreenName':
 
598
                        $replace = ($tweet->user->screen_name != '') ? ('@' . $tweet->user->screen_name) : '';
 
599
                        break;
 
600
 
 
601
                    case 'Date':
 
602
                        if($tweet->created_at != '')
 
603
                            $replace = $this->getDate()->getLocalDate(strtotime($tweet->created_at), $dateFormat);
 
604
                        break;
 
605
  
 
606
                    case 'Location':
 
607
                        $replace = $tweet->user->location;
 
608
                        break;
 
609
 
 
610
                    case 'ProfileImage':
 
611
                        // Grab the profile image
 
612
                        if ($tweet->user->profile_image_url != '') {
 
613
                            
 
614
                            // Original Default Image
 
615
                            $imageSizeType = "";
 
616
                            if( count($tagOptions) > 0 ) {
 
617
                              // Image options ( normal, bigger, mini )
 
618
                              $imageSizeType = '_' . $tagOptions[0];
 
619
                            }
 
620
                            
 
621
                            // Twitter image size
 
622
                            $tweet->user->profile_image_url = str_replace('_normal', $imageSizeType, $tweet->user->profile_image_url);
 
623
                            
 
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;
 
628
                            $file->save();
 
629
 
 
630
                            // Tag this layout with this file
 
631
                            $this->assignMedia($file->mediaId);
 
632
 
 
633
                            $replace = ($isPreview)
 
634
                                ? '<img src="' . $this->getApp()->urlFor('library.download', ['id' => $file->mediaId, 'type' => 'image']) . '?preview=1" />'
 
635
                                : '<img src="' . $file->storedAs . '"  />';
 
636
                        }
 
637
                        break;
 
638
                        
 
639
                    case 'Color':
 
640
                        // See if there is a profile image
 
641
                        if (!$this->tweetHasPhoto($tweet)) {
 
642
                        
 
643
                            // Get the colors array
 
644
                            $colorArray = explode("|", $this->getOption('templateColours'));
 
645
                            
 
646
                            // Find a random color
 
647
                            $randomNum = rand(0,count($colorArray)-1);
 
648
                            $randomColor = $colorArray[$randomNum];
 
649
                            
 
650
                            $replace = 'background-color:' . $randomColor;
 
651
                        }
 
652
                        break;
 
653
                        
 
654
                    case 'ShadowType':
 
655
                        // See if there is a profile image
 
656
                        $replace = ($this->tweetHasPhoto($tweet)) ? 'shadow darken-container' : '';
 
657
                        break;
 
658
 
 
659
                    case 'Photo':
 
660
                        // See if there are any photos associated with this tweet.
 
661
                        if ($this->tweetHasPhoto($tweet)) {
 
662
                            
 
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];
 
667
                            
 
668
                            $photoUrl = $mediaObject->media_url;
 
669
                            
 
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;
 
674
                                $file->save();
 
675
 
 
676
                                // Tag this layout with this file
 
677
                                $this->assignMedia($file->mediaId);
 
678
 
 
679
                                $replace = "background-image: url(" 
 
680
                                    . (($isPreview) ? $this->getApp()->urlFor('library.download', ['id' => $file->mediaId, 'type' => 'image']) : $file->storedAs)
 
681
                                    . ")";
 
682
                            }
 
683
                        }
 
684
                        break;
 
685
                        
 
686
                    case 'TwitterLogoWhite':
 
687
                        //Get the Twitter logo image file path
 
688
                        $replace = $this->getResourceUrl('twitter/twitter_white.png');
 
689
                        break;
 
690
                        
 
691
                    case 'TwitterLogoBlue':
 
692
                        //Get the Twitter logo image file path
 
693
                        $replace = $this->getResourceUrl('twitter/twitter_blue.png');
 
694
                        break;
 
695
 
 
696
                    default:
 
697
                        $replace = '[' . $subClean . ']';
 
698
                }
 
699
 
 
700
                $rowString = str_replace($sub, $replace, $rowString);
 
701
            }
 
702
 
 
703
            // Substitute the replacement we have found (it might be '')
 
704
            $return[] = $rowString;
 
705
        }
 
706
 
 
707
        // Return the data array
 
708
        return $return;
 
709
    }
 
710
 
 
711
    /**
 
712
     * Get Resource
 
713
     * @param int $displayId
 
714
     * @return mixed
 
715
     */
 
716
    public function getResource($displayId = 0)
 
717
    {
 
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');
 
721
            return '';
 
722
        }
 
723
 
 
724
        $data = [];
 
725
        $isPreview = ($this->getSanitizer()->getCheckbox('preview') == 1);
 
726
        
 
727
        // Replace the View Port Width?
 
728
        $data['viewPortWidth'] = ($isPreview) ? $this->region->width : '[[ViewPortWidth]]';
 
729
 
 
730
        // Information from the Module
 
731
        $duration = $this->getCalculatedDurationForGetResource();
 
732
 
 
733
        // Generate a JSON string of substituted items.
 
734
        $items = $this->getTwitterFeed($displayId, $isPreview);
 
735
        
 
736
        // Get the template data
 
737
        $templateData = $this->getTemplateData();
 
738
        
 
739
        // Return empty string if there are no items to show.
 
740
        if (count($items) == 0)
 
741
            return '';
 
742
 
 
743
        $options = array(
 
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'))            
 
757
        );
 
758
 
 
759
        // Replace the control meta with our data from twitter
 
760
        $data['controlMeta'] = '<!-- NUMITEMS=' . count($items) . ' -->' . PHP_EOL . '<!-- DURATION=' . $duration . ' -->';
 
761
 
 
762
        // Replace the head content
 
763
        $headContent = '';
 
764
 
 
765
        $backgroundColor = $this->getOption('backgroundColor');
 
766
        if ($backgroundColor != '') {
 
767
            $headContent .= '<style type="text/css">body, .page, .item { background-color: ' . $backgroundColor . ' }</style>';
 
768
        }
 
769
 
 
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">';
 
773
        
 
774
        // Add the CSS if it isn't empty
 
775
        $css = $templateData['styleSheet'];
 
776
        
 
777
        if ($css != '') {
 
778
            $headContent .= '<style type="text/css">' . $this->parseLibraryReferences($isPreview, $css) . '</style>';
 
779
        }
 
780
        $headContent .= '<style type="text/css">' . file_get_contents($this->getConfig()->uri('css/client.css', true)) . '</style>';
 
781
 
 
782
        // Replace the Head Content with our generated javascript
 
783
        $data['head'] = $headContent;
 
784
 
 
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>';
 
787
 
 
788
        // Get the colors array
 
789
        $colorArray = explode("|", $this->getOption('templateColours'));
 
790
        
 
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>';
 
794
 
 
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>';
 
797
 
 
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>';
 
806
 
 
807
        // Replace the Head Content with our generated javascript
 
808
        $data['javaScript'] = $javaScriptContent;
 
809
 
 
810
        // Update and save widget if we've changed our assignments.
 
811
        if ($this->hasMediaChanged())
 
812
            $this->widget->save(['saveWidgetOptions' => false, 'notifyDisplays' => true]);
 
813
 
 
814
        return $this->renderTemplate($data);
 
815
    }
 
816
 
 
817
    public function isValid()
 
818
    {
 
819
        // Using the information you have in your module calculate whether it is valid or not.
 
820
        // 0 = Invalid
 
821
        // 1 = Valid
 
822
        // 2 = Unknown
 
823
        return 1;
 
824
    }
 
825
    
 
826
    public function tweetHasPhoto($tweet)
 
827
    {
 
828
        return ((isset($tweet->entities->media) && count($tweet->entities->media) > 0) || (isset($tweet->retweeted_status->entities->media) && count($tweet->retweeted_status->entities->media) > 0));
 
829
    }
 
830
}