Assembla home | Assembla project page
 

root/trunk/Phergie/Plugin/FeedTicker.php

Revision 280, 11.0 kB (checked in by Seldaek, 2 months ago)

+ Added feedticker ini setting to disable the buffering : feedticker.smart_buffer (defaults to true)

Line 
1 <?php
2
3 /**
4  * Sporadically syndicates items from a given set of feeds to the channel.
5  */
6 class Phergie_Plugin_FeedTicker extends Phergie_Plugin_Abstract_Cron
7 {
8     /**
9      * Determines if the plugin is a passive plugin or not
10      *
11      * @var bool
12      */
13     public $passive = true;
14
15     /**
16      * Delay in seconds for syndicating feeds, set to 30 minutes
17      *
18      * @var int
19      */
20     protected $defaultDelay = 1800;
21
22     /**
23      * Delay in minutes between checking the queue, set to 5 minutes
24      *
25      * @var int
26      */
27     protected $postThrottle = 5;
28
29     /**
30      * Feed data
31      *
32      * @see run()
33      * @var array
34      */
35     protected $feeds = null;
36
37     /**
38      * Filter feed title data
39      *
40      * @see run()
41      * @var array
42      */
43     protected $filterTitle = null;
44
45     /**
46      * Filter feed url data
47      *
48      * @see run()
49      * @var array
50      */
51     protected $filterUrl = null;
52
53     /**
54      * Filter article title data
55      *
56      * @see run()
57      * @var array
58      */
59     protected $filterArticle = null;
60
61     /**
62      * Cache of the last update to check for new entries
63      *
64      * @see run()
65      * @var array
66      */
67     protected $cache = null;
68
69     /**
70      * Queue of items to be dispatched
71      *
72      * @see checkQueue()
73      * @see run()
74      * @var array
75      */
76     protected $queue = array();
77
78     /**
79      * Time at which the checkQueue method will be allowed to dispatch
80      * another item
81      *
82      * @see checkQueue()
83      * @var int
84      */
85     protected $nextOutput = null;
86
87     /**
88      * Feed output format; can use the variables %title%, %link% and %feed% to
89      * display article titles, links and feed titles
90      *
91      * @var string
92      */
93     protected $format = '%title% [ %link% ]';
94
95     /**
96      * If true, the new feed items are buffered until something happens on
97      * the channel, indicating some kind of presence / readership is available
98      *
99      * @var bool
100      */
101     protected $smartBuffer = true;
102
103     /**
104      * Processes necessary configuration setting values.
105      *
106      * @return void
107      */
108     public function onInit()
109     {
110         // Delay between feed syndications
111         $fetchDelay = intval($this->getPluginIni('fetch'));
112         if ($fetchDelay > 0) {
113             $this->defaultDelay = $fetchDelay * 60;
114         }
115
116         // Post throttle
117         $postThrottle = intval($this->getPluginIni('post'));
118         if ($postThrottle > 0) {
119             $this->postThrottle = $postThrottle;
120         }
121
122         // Global Feed Title, Feed URL and Article Title Filters
123         $globalTitle = trim($this->getPluginIni('filter_title'));
124         $globalUrl = trim($this->getPluginIni('filter_url'));
125         $globalArticle = trim($this->getPluginIni('filter_article'));
126         if ($this->getPluginIni('smart_buffer') !== null) {
127             $this->smartBuffer = (bool) $this->getPluginIni('smart_buffer');
128         }
129
130         $i = 0;
131         $this->feeds = array();
132         do {
133             // Feed and Chan data
134             $feed = $this->getPluginIni('feed' . $i);
135             $chans = $this->getPluginIni('chans' . $i);
136             // Feed Title, Feed URL and Article Title Filter Data
137             $filterTitle = $this->getPluginIni('filter_title' . $i);
138             $filterUrl = $this->getPluginIni('filter_url' . $i);
139             $filterArticle = $this->getPluginIni('filter_article' . $i);
140
141             if (!empty($feed) && !empty($chans)) {
142                 $this->feeds[] = array($feed, preg_split('#[\s\r\n,]+#', $chans));
143                 // Feed Title, Feed URL and Article Title Filters
144                 $filterTrim = "| \t\n\r\0\v\0xa0";
145                 $this->filterTitle[] = trim(implode('|', array($globalTitle, $filterTitle)), $filterTrim);
146                 $this->filterUrl[] = trim(implode('|', array($globalUrl, $filterUrl)), $filterTrim);
147                 $this->filterArticle[] = trim(implode('|', array($globalArticle, $filterArticle)), $filterTrim);
148             }
149         } while (++$i < 10);
150         if ($this->getPluginIni('format') != null) {
151             $this->format = $this->getPluginIni('format');
152         }
153     }
154
155     /**
156      * Returns whether or not the plugin's dependencies are met.
157      *
158      * @param Phergie_Driver_Abstract $client Client instance
159      * @param array $plugins List of short names for plugins that the
160      *                       bootstrap file intends to instantiate
161      * @see Phergie_Plugin_Abstract_Base::checkDependencies()
162      * @return bool TRUE if dependencies are met, FALSE otherwise
163      */
164     public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
165     {
166         $errors = array();
167
168         if (!extension_loaded('SimpleXML')) {
169             $errors[] = 'SimpleXML php extension is required';
170         }
171         if (!self::staticPluginLoaded('TinyUrl', $client, $plugins)) {
172             $errors[] = 'TinyUrl plugin must be enabled';
173         }
174
175         return empty($errors) ? true : $errors;
176     }
177
178     /**
179      * Whenever a user sends a message to a channel in which the bot is present,
180      * the queue is checked. This behavior attempts to prevent the bot from
181      * spamming the channel if no users are conversing and everything is retained
182      * in the queue until channel activity is detected.
183      *
184      * @return void
185      */
186     public function onPrivmsg()
187     {
188         $this->checkQueue();
189     }
190
191     /**
192      * Checks the queue for new items and send out one if the necessary time
193      * limit has elapsed since the last item was sent. Uses the format setting
194      * to format new items.
195      *
196      * @return void
197      */
198     protected function checkQueue()
199     {
200         if (!empty($this->queue) && time() > $this->nextOutput) {
201             list($title, $url, $chans, $feedTitle) = array_pad(array_shift($this->queue), 4, null);
202             foreach($chans as $chan) {
203                 $this->doPrivmsg($chan, str_replace(array(
204                     '%title%',
205                     '%link%',
206                     '%feed%'
207                 ), array(
208                     $title,
209                     $url,
210                     $feedTitle
211                 ), $this->format));
212             }
213             $this->nextOutput = time() + ($this->postThrottle * 60);
214         }
215     }
216
217     /**
218      * Retrieves feeds and fills the queue with new items that were not
219      * previously in the cache.
220      *
221      * Technical data if you want to extend this method to parse another type
222      * of source differently :
223      *
224      * Feeds are arrays such as: array("feed url", array("chan1", "chan2"))
225      *
226      * Cache management is up to you and is only internally used by this
227      * method.
228      *
229      * The queue is an array of items to be dispatched, these items are as
230      * such :
231      *   array("item title", "item url", array("chan1", "chan2"), "feed title")
232      *
233      * @return void
234      */
235     protected function run()
236     {
237         $retrieved = array();
238
239         $context = stream_context_create(array(
240             'http' => array(
241                 'timeout' => 5,
242                 'user_agent' => 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9b3) Gecko/2008020514 Firefox/3.0b3'
243             )
244         ));
245
246         // Retrieve each feed
247         foreach($this->feeds as $id => $feed) {
248             list($url, $chans) = array_pad($feed, 2, null);
249
250             $content = @file_get_contents($url, null, $context);
251             if (empty($content)) {
252                 $this->debug('Feed empty: ' . $url);
253                 continue;
254             }
255
256             try {
257                 // RSS/RDF Feed
258                 if (preg_match('/<rss[^>]+version=/', $content) || stripos($content, '<rdf') !== false) {
259                     $xml = new SimpleXMLElement($content);
260                     $feedTitle = (string)$xml->channel->title;
261                     foreach($xml->channel->item as $item) {
262                         $retrieved[$id][] = array((string) $item->title, (string) $item->link, $chans, $feedTitle);
263                     }
264                 } elseif (stripos($content, '/Atom') !== false) { // ATOM Feed
265                     $xml = new SimpleXMLElement($content);
266                     $feedTitle = (string)$xml->title;
267                     foreach($xml->entry as $item) {
268                         $retrieved[$id][] = array((string) $item->title, (string) $item->link[0]['href'], $chans, $feedTitle);
269                     }
270                 } else { // Trouble
271                     $this->debug('Feed format unrecognized: ' . $url);
272                     continue;
273                 }
274             } catch (Exception $e) {
275                 $this->debug('Caught exception: ', $e->getMessage());
276                 continue;
277             }
278         }
279
280         // First run, fill cache and don't output anything
281         if ($this->cache === null) {
282             $this->cache = $retrieved;
283             return;
284         }
285
286         // Latter run, compare retrieved data to cache and queue new items
287         foreach($retrieved as $id => $articles) {
288             $articles = array_reverse($articles);
289             foreach($articles as $article) {
290                 if ((!isset ($this->cache[$id]) ||
291                     array_search($article, $this->cache[$id]) === false) &&
292                     $this->filterCheck($id, $article[0], $article[1], $article[3])) {
293                     // Decode and trim article title
294                     $article[0] = $this->decode($article[0], 40);
295                     // Convert link with TinyURL if required
296                     $article[1] = Phergie_Plugin_TinyUrl::get($article[1]);
297                     // Decode and trim feed title
298                     $article[3] = $this->decode($article[3], 20);
299                     $this->queue[] = $article;
300                 }
301             }
302             // Cache current data for next run
303             $this->cache[$id] = $articles;
304         }
305
306         if (!$this->smartBuffer) {
307             $this->checkQueue();
308         }
309     }
310
311     // Checks the given feed Title and URL as well as article title against the feed filters
312     protected function filterCheck($id, $title, $url, $article)
313     {
314         // Feed Title, Feed URL and Article Title Filters
315         $filterTitle = $this->filterTitle[$id];
316         $filterUrl = $this->filterUrl[$id];
317         $filterArticle = $this->filterArticle[$id];
318
319         // Check against the filters if any are set
320         if (($filterTitle && preg_match('{'.$filterTitle.'}im', $title, $match)) ||
321             ($filterUrl && preg_match('{'.$filterUrl.'}im', $url, $match)) ||
322             ($filterArticle && preg_match('{'.$filterArticle.'}im', $article, $match))) {
323             return false;
324         }
325         return true;
326     }
327
328     /**
329      * Transliterates a UTF-8 string into corresponding ASCII characters and
330      * truncates and appends an ellipsis to the string if it exceeds a given
331      * length.
332      *
333      * @param string $str String to decode
334      * @param int $trim Maximum string length, optional
335      * @return string
336      */
337     protected function decode($str, $trim = null)
338     {
339         $out = $this->decodeTranslit($str);
340         if ($trim > 0) {
341             $out = substr($out, 0, $trim) . (strlen($out) > $trim ? '...' : '');
342         }
343         return $out;
344     }
345 }
346
Note: See TracBrowser for help on using the browser.