Assembla home | Assembla project page
 

root/trunk/Phergie/Plugin/ChuckNorris.php

Revision 273, 9.9 kB (checked in by Seldaek, 2 months ago)

* Plugins that can't load now return error messages to say why
fixes #35

Line 
1 <?php
2
3 /**
4  * Parses incoming messages for the word "Chuck Norris" and respond with a
5  * random Chuck Norris fact retrieved from the Chuck Norris fact page. The
6  * plugin will also scrape the fact page and store all the facts it retrieves
7  * into a database for quick access.
8  */
9 class Phergie_Plugin_ChuckNorris extends Phergie_Plugin_Abstract_Base
10 {
11
12     /**
13      * Indicates that a local directory is required for this plugin
14      *
15      * @var bool
16      */
17     protected $needsDir = true;
18
19     /**
20      * Stores the SQLite object
21      *
22      * @var resource
23      */
24     protected $db = null;
25
26     /**
27      * The host of the Chuck Norris facts page we use for our information
28      *
29      * @var string
30      */
31     protected $factHost = 'http://chucknorrisfacts.com/';
32
33     /**
34      * The starting page we use to scrape out information from
35      *
36      * @var string
37      */
38     protected $crawlPage = 'new.html';
39
40     /**
41      * The current page number when crawling
42      *
43      * @var int
44      */
45     protected $crawlNum = 0;
46
47     /**
48      * Stores all the Chuck Norris facts while the pages are being parsed
49      *
50      * @var array
51      */
52     protected $norrisFacts = array();
53
54     /**
55      * Flood check period in seconds
56      * Set to 0 or below to disable.
57      *
58      * @var int
59      */
60     protected $floodCheck = 30;
61
62     /**
63      * The chance of responding with a random Chuck Norris fact. The chance can
64      * bet set anywheres from 1 - 100 while a value like 50 is a 50% chance.
65      * Set to 0 or below or a value of 100 or higher to disable.
66      *
67      * @var int
68      */
69     protected $chance = 50;
70
71     /**
72      * Stores the timestamp of the last use and is used in flood check comparisons
73      *
74      * @var int
75      */
76     protected $floodCache = array();
77
78     /**
79      * Connects to the database and populates the fact table when needed.
80      *
81      * @return void
82      */
83     public function onInit()
84     {
85         try {
86             // Initialize the database connection
87             $this->db = new PDO('sqlite:' . $this->dir . 'norris.db');
88             if (!$this->db) {
89                 return;
90             }
91
92             // Populate the database if necessary
93             // Checks to see if the table exists, if not create it
94             if ($this->findChuckNorris()) {
95                 // Retrieves a list of Chuck Norris facts
96                 if ($this->getChuckNorris()) {
97                     // Inserts all the retrieved facts into the database
98                     $this->feedChuckNorris();
99                 }
100             }
101         } catch (PDOException $e) { }
102
103         $this->chance = intval($this->chance);
104         $this->floodCheck = intval($this->floodCheck);
105     }
106
107     /**
108      * Returns whether or not the plugin's dependencies are met.
109      *
110      * @param Phergie_Driver_Abstract $client Client instance
111      * @param array $plugins List of short names for plugins that the
112      *                       bootstrap file intends to instantiate
113      * @see Phergie_Plugin_Abstract_Base::checkDependencies()
114      * @return bool TRUE if dependencies are met, FALSE otherwise
115      */
116     public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
117     {
118         $errors = array();
119
120         if (!extension_loaded('PDO')) {
121             $errors[] = 'PDO php extension is required';
122         }
123         if (!extension_loaded('pdo_sqlite')) {
124             $errors[] = 'pdo_sqlite php extension is required';
125         }
126
127         return empty($errors) ? true : $errors;
128     }
129
130     /**
131      * Connects to the Chuck Norris facts page and scrapes the facts from it
132      *
133      * @return bool True is successful, else false
134      */
135     private function getChuckNorris($doCrawl = true)
136     {
137         // The current page to retrieve
138         $page = trim($this->factHost . $this->crawlPage);
139
140         $this->debug('Retrieving page ' . $this->crawlNum . ' => ' . $this->crawlPage);
141         $contents = @file_get_contents($page);
142         if ($contents !== false) {
143             $this->debug('Parsing page ' . $this->crawlNum . ' => ' . $this->crawlPage);
144             preg_match_all('#<li>\s*([^<]+)\s*(?:<br>\s*&nbsp;)?<\/li>#im', $contents, $matches);
145
146             // Format the returned facts to be stored later in a database
147             foreach($matches[1] as $key => $fact) {
148                 $fact = trim(preg_replace("/(&nbsp;|[\r\n\s])+/", ' ', html_entity_decode($fact, ENT_QUOTES)));
149                 $fact = str_replace(array('&#8217;','&#8220;','&#8221;','&#8230;'), array("'",'"','"','...'), $fact);
150                 if (!empty($fact)) {
151                     $this->norrisFacts[] = $fact;
152                 }
153             }
154
155             // If crawling is set, crawl and scrape the remaining fact pages
156             if ($doCrawl) {
157                 preg_match_all('#<a href="(page([0-9]+)\.html)">#im', $contents, $matches);
158
159                 // Make sure we don't have any duplicates
160                 $pages = array_unique(array_combine($matches[2], $matches[1]));
161                 ksort($pages, SORT_NUMERIC);
162
163                 // Start the crawling processs
164                 foreach($pages as $page => $url) {
165                     if ($page > $this->crawlNum) {
166                         $this->crawlNum = $page;
167                         $this->crawlPage = $url;
168
169                         // Return false if there was a problem with the crawling
170                         if (!$this->getChuckNorris(false)) {
171                             return false;
172                         }
173                     }
174                 }
175             }
176             unset($contents);
177             return true;
178         }
179
180         $this->debug('Could not retrieve page ' . $this->crawlNum . ' => ' . $this->crawlPage);
181         return false;
182     }
183
184     /**
185      * Determines if the chuckfacts does not exist or empty
186      *
187      * @return bool TRUE if the table does not exist or is empty, FALSE
188      *              otherwise
189      */
190     private function findChuckNorris()
191     {
192         if (!$this->db) {
193             return false;
194         }
195
196         // Checks to see if the chuckfacts table exists
197         $table = $this->db->query('SELECT COUNT(*) FROM sqlite_master WHERE name = ' . $this->db->quote('chuckfacts'))->fetchColumn();
198
199         // If the table doesn't exist, create them and return true for the next step
200         if (!$table) {
201             $this->debug('Creating the database schema');
202             $this->db->exec('CREATE TABLE chuckfacts (facts VARCHAR(255))');
203             $this->db->exec('CREATE UNIQUE INDEX chuckfacts_name ON chuckfacts (facts)');
204             return true;
205         }
206
207         // Checks to see if anything is stored in the chuckfacts table
208         return !$this->db->query('SELECT COUNT(*) FROM chuckfacts')->fetchColumn();
209     }
210
211     /**
212      * Populates the chuckfacts table with the given array of facts
213      *
214      * @return bool True is successful, else false
215      */
216     private function feedChuckNorris()
217     {
218         if (!$this->db) {
219             return false;
220         }
221
222         // Check to see if there are any facts to insert
223         if (count($this->norrisFacts) > 0) {
224             $stmt = $this->db->prepare('INSERT INTO chuckfacts (facts) VALUES (:fact)');
225             $this->db->beginTransaction();
226             // Go through the facs and insert them if available
227             foreach(array_unique($this->norrisFacts) as $fact) {
228                 if (!empty($fact)) {
229                     $stmt->execute(array(':fact' => $fact));
230                     $this->debug('Inserted fact: ' . $fact);
231                 }
232             }
233             $this->db->commit();
234             // Unset the facts array to free up memory
235             unset($this->norrisFacts);
236
237             return true;
238         }
239         return false;
240     }
241
242     /**
243      * Returns a random fact from the chuckfacts table.
244      *
245      * @return string A random fact
246      */
247     private function praiseChuckNorris()
248     {
249         if (!$this->db) {
250             return;
251         }
252
253         return $this->db->query('SELECT facts FROM chuckfacts ORDER BY Random() LIMIT 1')->fetchColumn();
254     }
255
256     /**
257      * Parses incoming messages for the word "Chuck Norris" and respond with a
258      * random Chuck Norris fact retrieved from the Chuck Norris fact page.
259      *
260      * @return void
261      */
262     public function onPrivmsg()
263     {
264         if (!$this->db) {
265             return;
266         }
267
268         $source = $this->event->getSource();
269         $message = $this->event->getArgument(1);
270
271         // Check to see if the message includes the word Chuck Norris. If so, check to see if
272         // it was a bot request by checking for Fact: at the begiining and also do a floodpro check
273         if (preg_match('{^('.preg_quote($this->getIni('nick')).'\s*[:,>]?\s+)?\s*(chuck\s+norris)}ix', $message, $m) &&
274             strtolower(substr($message, 0, 5)) != 'fact:' && ($source[0] != '#' ||
275             (!empty($m[1]) || $this->chance <= 0 || $this->chance >= 100 || mt_rand(1, 100) < $this->chance) &&
276             ($this->floodCheck <= 0 || !isset($this->floodCache[$source]) ||
277             ($this->floodCache[$source] < (time() - $this->floodCheck))))) {
278             $fact = $this->praiseChuckNorris();
279             if (!empty($fact)) {
280                 $this->doPrivmsg($source, 'Fact: ' . $fact);
281                 if ($source[0] == '#') {
282                     $this->floodCache[$source] = time();
283                 }
284                 unset($m, $fact);
285             }
286         }
287     }
288
289     /**
290      * Parses incoming CTCP request for the word "Chuck Norris" and respond with
291      * a random Chuck Norris fact retrieved from the Chuck Norris fact page.
292      *
293      * @return void
294      */
295     public function onCtcp()
296     {
297         $source = $this->event->getSource();
298         $ctcp = $this->event->getArgument(1);
299
300         if (!$this->db) {
301             return;
302         }
303
304         if (preg_match('{chuck[\s_+-]*norris}ix', $ctcp, $m)) {
305             $fact = $this->praiseChuckNorris();
306             if (!empty($fact)) {
307                 $this->doCtcpReply($source, 'CHUCKNORRIS', $fact);
308             }
309         }
310     }
311 }
312
Note: See TracBrowser for help on using the browser.