Assembla home | Assembla project page
 

root/trunk/Phergie/Plugin/Drink.php

Revision 281, 14.7 kB (checked in by Slynderdale, 2 months ago)

Fixed a bug in Drink when referring to the bot by name.

Line 
1 <?php
2
3 /**
4  * Parses incoming messages for issued commands indicating that the bot should
5  * serve a specified user with a random drink of a specific type, locates a
6  * drink of that type, and responds with an action indicating that the bot is
7  * serving that drink to that user.
8  *
9  * The gender configuration setting should be set to either M or F to indicate
10  * the gender of the bot so as to allow for substitution of the proper pronoun
11  * when a user requests that the bot serve itself a drink.
12  */
13 class Phergie_Plugin_Drink extends Phergie_Plugin_Abstract_Command
14 {
15     /**
16      * Indicates that a local directory is required for this plugin
17      *
18      * @var bool
19      */
20     protected $needsDir = true;
21
22     /**
23      * PDO resource for a SQLite database containing the drinks
24      *
25      * @var resource
26      */
27     protected $db = null;
28
29     /**
30      * List of profane words to filter from imported drinks
31      *
32      * @var array
33      */
34     protected $filter = array(
35         'shit',
36         'shitkicker',
37         'shitface',
38         'shittin',
39         'piss',
40         'pissed',
41         'pisser',
42         'pissbomb',
43         'fuck',
44         'fucks',
45         'motherfucker',
46         'motherfuckers',
47         'fucked',
48         'fucker',
49         'dumbfuck',
50         'mindfuck',
51         'stumblefuck',
52         'cunt',
53         'cock',
54         'cockteaser',
55         'cocksucker',
56         'ass',
57         'asshole',
58         'bitch',
59         'bitches',
60         'bitchin',
61         'tit',
62         'tits',
63         'titty',
64         'titties',
65         'penis',
66         'penisbutter',
67         'dick',
68         'dickhead',
69         'sex',
70         'buttsex',
71         'sexoholic',
72         'sexual',
73         'sexy',
74         'triplesex',
75         'panties',
76         'urine'
77     );
78
79     /**
80      * Connects to the database and populates tables where needed.
81      *
82      * @return void
83      */
84     public function onInit()
85     {
86         try {
87             // Initialize the database connection
88             $this->db = new PDO('sqlite:' . $this->dir . 'drink.db');
89             if (!$this->db) {
90                 return;
91             }
92
93             // Populate the database if necessary
94             if ($this->needTable('beer')) {
95                 $this->debug('Retrieving data for: Beer');
96                 $contents = @file_get_contents('http://beerme.com/beerlist.php');
97                 if ($contents !== false) {
98                     $this->debug('Parsing data for: Beer');
99                     preg_match_all('/brewery\.php\?[0-9]+#[0-9]+\'>([^<]+)/', $contents, $matches);
100                     $names = array();
101                     foreach($matches[1] as $key => $name) {
102                         $name = $this->decodeTranslit($name);
103                         if ($this->hasBadChars($name) || strpos($name, '(discontinued)') !== false) {
104                             continue;
105                         }
106                         $name = explode('/', preg_replace('/\([^)]+\)/', '', $name));
107                         $name = trim(array_shift($name));
108                         if (!empty($name)) {
109                             $name = html_entity_decode($name);
110                             $names[] = $name;
111                         }
112                     }
113                     $this->populateTable('beer', $names);
114                     unset($names);
115                 }
116             }
117
118             if ($this->needTable('cocktail')) {
119                 $limit = 2;
120                 $names = array();
121                 $this->debug('Retrieving data for: Cocktail');
122                 for ($i = 1; $i <= $limit; $i += 150) {
123                     $contents = @file_get_contents('http://www.webtender.com/db/browse?level=2&dir=drinks&char=%2A&start=' . $i);
124                     if ($contents === false) {
125                         break;
126                     }
127                     $this->debug('Parsing data for: Cocktail (' . $i . ' - ' . ($i + 150) . ')');
128                     if ($i == 1) {
129                         preg_match('/>([0-9]+) found\\.</', $contents, $match);
130                         $limit = $match[1]+(150-($match[1]%150));
131                     }
132                     preg_match_all('/db\\/drink\\/[0-9]+">([^<]+)/', $contents, $matches);
133                     foreach($matches[1] as $name) {
134                         $name = $this->decodeTranslit($name);
135                         if ($this->hasBadChars($name)) {
136                             continue;
137                         }
138                         $name = html_entity_decode(preg_replace('/ The$|^The |\s*\([^)]+\)\s*| #[0-9]+$/', '', $name));
139                         $names[] = $name;
140                     }
141                 }
142                 if ($contents) {
143                     $this->populateTable('cocktail', $names);
144                 }
145                 unset($names);
146             }
147
148             if ($this->needTable('coke')) {
149                 $this->debug('Retrieving data for: Coke');
150                 $contents = @file_get_contents('http://www.energyfiend.com/huge-caffeine-database/');
151                 if ($contents) {
152                     $this->debug('Parsing data for: Coke');
153                     // List of drinks to filter out
154                     $filter = array(
155                         'tea',
156                         'coffee',
157                         'starbucks'
158                     );
159                     $start = stripos($contents, 'id="caffeinedb"');
160                     $end = stripos($contents, '</table>', $start);
161                     $contents = substr($contents, $start, $end-$start);
162                     preg_match_all('/<tr[^>]*><td>(<[^>]+>)?([^<]+)/is', $contents, $matches);
163                     $names = array();
164                     foreach($matches[2] as $name) {
165                         $name = $this->decodeTranslit($name);
166                         if ($this->hasBadChars($name)) {
167                             continue;
168                         }
169                         $name = html_entity_decode(trim(preg_replace('/ \\([^)]+\\)| - .*$/', '', $name)));
170                         if (!preg_match('/(?:^|\s+)(?:' . implode('|', $filter) . ')(?:\s+|$)/i', $name)) {
171                             $names[] = $name;
172                         }
173                     }
174                     $this->populateTable('coke', $names);
175                     unset($names);
176                 }
177             }
178
179             if ($this->needTable('tea')) {
180                 $this->debug('Retrieving data for: Tea');
181                 $names = @file('http://www.midnight-labs.org/tea.txt');
182                 if ($names) {
183                     $this->debug('Parsing data for: Tea');
184                     foreach($names as $key => $value) {
185                         $value = $this->decodeTranslit($value);
186                         if ($this->hasBadChars($value)) {
187                             continue;
188                         }
189                         $names[$key] = ucwords(trim($value));
190                     }
191                     $this->populateTable('tea', $names);
192                     unset($names);
193                 }
194             }
195         } catch (PDOException $e) { }
196
197         unset($this->filter);
198     }
199
200     /**
201      * Returns whether or not the plugin's dependencies are met.
202      *
203      * @param Phergie_Driver_Abstract $client Client instance
204      * @param array $plugins List of short names for plugins that the
205      *                       bootstrap file intends to instantiate
206      * @see Phergie_Plugin_Abstract_Base::checkDependencies()
207      * @return bool TRUE if dependencies are met, FALSE otherwise
208      */
209     public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
210     {
211         $errors = array();
212
213         if (!extension_loaded('PDO')) {
214             $errors[] = 'PDO php extension is required';
215         }
216         if (!extension_loaded('pdo_sqlite')) {
217             $errors[] = 'pdo_sqlite php extension is required';
218         }
219
220         return empty($errors) ? true : $errors;
221     }
222
223     /**
224      * Determines if a table does not exist or is empty.
225      *
226      * @param string $name Table name
227      * @return bool TRUE if the table does not exist or is empty, FALSE
228      *              otherwise
229      */
230     private function needTable($name)
231     {
232         $table = $this->db->query('SELECT COUNT(*) FROM sqlite_master WHERE name = ' . $this->db->quote($name))->fetchColumn();
233         if (!$table) {
234             return true;
235         }
236
237         return !$this->db->query('SELECT COUNT(*) FROM ' . $name)->fetchColumn();
238     }
239
240     /**
241      * Populates a source database table with a given set of data.
242      *
243      * @param string $table Name of the table
244      * @param array $names List of drink names
245      * @return void
246      */
247     private function populateTable($table, $names)
248     {
249         $this->debug('Creating the database schema for: ' . ucfirst($table));
250         $this->db->exec('CREATE TABLE ' . $table . ' (name VARCHAR(255))');
251         $this->db->exec('CREATE UNIQUE INDEX ' . $table . '_name ON ' . $table . ' (name)');
252
253         $stmt = $this->db->prepare('INSERT INTO ' . $table . ' (name) VALUES (:name)');
254         $this->db->beginTransaction();
255         foreach(array_unique($names) as $name) {
256             if (preg_match('/(?:^|[^a-z])(' . implode('|', $this->filter) . ')(?:[^a-z]|$)/i', $name, $match)) {
257                 $this->debug('Filtered out ' . $name . ' because it contains ' . $match[1]);
258                 continue;
259             }
260             $this->debug('Inserted ' . ucfirst($table) . ': ' . $name);
261             $stmt->execute(array('name' => $name));
262         }
263         $this->db->commit();
264     }
265
266     /**
267      * Returns a random record value from a given table.
268      *
269      * @string $table Name of the table
270      * @return string Value of the name column for the selected record
271      */
272     private function getRandomRecord($table)
273     {
274         return $this->db->query('SELECT name FROM ' . $table . ' ORDER BY RANDOM() LIMIT 1')->fetchColumn();
275     }
276
277     /**
278      * Returns a random record value from a given search pattern.
279      *
280      * @string $table Name of the table
281      * @return string Value of the name column for the selected record
282      */
283     private function getSearchRecord($table, $search)
284     {
285         $search = $this->db->quote('%' . str_replace(array('\\', '%'), array('\\\\', '\\%'), $search) .'%');
286         return $this->db->query('SELECT name FROM ' . $table . ' WHERE name LIKE ' . $search . ' ESCAPE "\\" ORDER BY RANDOM() LIMIT 1')->fetchColumn();
287     }
288
289     /**
290      * Returns whether or not a given value has characters that may not be
291      * displayed correctly.
292      *
293      * @param string $name Value to check
294      */
295     private function hasBadChars($name)
296     {
297         return (max(array_map('ord', str_split($name))) > 126);
298     }
299
300     /**
301      * Resolves a target to the appropriate nick or pronoun and returns the
302      * result.
303      *
304      * @param string $target Original specified target
305      * @return string Resolved target
306      */
307     protected function resolveTarget($target)
308     {
309         $target = rtrim(trim($target), '.?!');
310
311         switch (trim(strtolower($target))) {
312             case 'me':
313                 $target = $this->event->getNick();
314             break;
315
316             case 'you':
317             case 'your self':
318             case 'yourself':
319             case strtolower($this->getIni('nick')):
320                 $gender = $this->getIni('gender');
321                 if (!$gender || $gender == 'F') {
322                     $target = 'herself';
323                 } else {
324                     $target = 'himself';
325                 }
326             break;
327         }
328
329         return $target;
330     }
331
332     /**
333      * Responds to a message requesting that the bot perform an action to
334      * serve the source with a random drink of a specific type.
335      *
336      * @param string $type Type of drink
337      * @param string $message Drink reuest message
338      * @return void
339      */
340     protected function handleDrink($type, $message)
341     {
342         if (!$this->db) {
343             return;
344         }
345
346         $message = preg_replace('/\s+/', ' ', trim($message));
347         preg_match('/^(.+?)(?:\s+an?(?:\s+(?:cuppa|cup\s+of))?\s+(.+?))?(\s+(?:for|because)\s+.+)?$/', $message, $m);
348         list(, $target, $drink, $action) = array_pad($m, 4, null);
349
350         $drink = trim($drink);
351         if ($type == 'tea' && strtolower(substr($drink, -3)) == 'tea') {
352             $drink = trim(substr($drink, 0, -3));
353         }
354
355         $action = trim($action);
356         $target = $this->resolveTarget($target);
357         if (!empty($drink)) {
358             $drink = implode(' ', array_map('ucfirst', explode(' ', $drink)));
359             if ($search = $this->getSearchRecord($type, $drink)) {
360                 $drink = $search;
361             }
362         } else {
363             $drink = $this->getRandomRecord($type);
364         }
365
366         if ($drink) {
367             if ($type != 'tea') {
368                 $text = 'throws ' . $target . ' a';
369                 if (preg_match('/^[aeoiu]/i', $drink)) {
370                     $text .= 'n';
371                 }
372                 $text .= ' ' . $drink . ($action ? ' ' . $action : '');
373             } else {
374                 // One must be gentle with tea
375                 $text = 'pours ' . $target . ' a cup of ' . $drink . ' tea'  . ($action ? ' ' . $action : '');
376             }
377             $this->doAction($this->event->getSource(), $text . '.');
378         }
379     }
380
381     /**
382      * Handles beer requests.
383      *
384      * @param string $target Target for the request
385      * @return void
386      */
387     public function onDoBeer($message)
388     {
389         $this->handleDrink('beer', $message);
390     }
391
392     /**
393      * Alias to Beer
394      *
395      * @param string $target Target for the request
396      * @return void
397      */
398     public function onDoBooze($message)
399     {
400         $this->handleDrink('beer', $message);
401     }
402
403     /**
404      * Handles cocktail requests.
405      *
406      * @param string $target Target for the request
407      * @return void
408      */
409     public function onDoCocktail($message)
410     {
411         $this->handleDrink('cocktail', $message);
412     }
413
414     /**
415      * Handles coke requests.
416      *
417      * @param string $target Source of the request
418      * @return void
419      */
420     public function onDoCoke($message)
421     {
422         $this->handleDrink('coke', $message);
423     }
424
425     /**
426      * Provides a soda alias for coke requests.
427      *
428      * @param string $target Target for the request
429      * @return void
430      */
431     public function onDoSoda($message)
432     {
433         $this->handleDrink('coke', $message);
434     }
435
436     /**
437      * Handles tea requests.
438      *
439      * @param string $target Target for the request
440      * @return void
441      */
442     public function onDoTea($message)
443     {
444         $this->handleDrink('tea', $message);
445     }
446
447     /**
448      * Handles pop requests.
449      *
450      * @param string $target Target for the request
451      * @return void
452      */
453     public function onDoPop($target)
454     {
455         $target = $this->resolveTarget($target);
456
457         $this->doAction($this->event->getSource(), 'lays ' . $target . ' out flat.');
458     }
459 }
460
Note: See TracBrowser for help on using the browser.