Assembla home | Assembla project page
 

root/trunk/Phergie/Plugin/Lart.php

Revision 273, 15.0 kB (checked in by Seldaek, 1 month ago)

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

Line 
1 <?php
2
3 /**
4  * Accepts terms and corresponding definitions for storage to a local data
5  * source and performs and returns the result of lookups for term definitions
6  * as they are requested.
7  */
8 class Phergie_Plugin_Lart extends Phergie_Plugin_Abstract_Base
9 {
10     /**
11      * Indicates that a local directory is required for this plugin
12      *
13      * @var bool
14      */
15     protected $needsDir = true;
16
17     /**
18      * Date string indicating the last time the cache was emptied
19      *
20      * @var string
21      */
22     protected $flushed;
23
24     /**
25      * Maps terms to corresponding definitions to serve as an in-memory cache
26      *
27      * @var array
28      */
29     protected $cache;
30
31     /**
32      * PDO instance for the database
33      *
34      * @var PDO
35      */
36     protected $db;
37
38     /**
39      * Prepared statement for inserting a new definition into or updating an
40      * existing definition in the database
41      *
42      * @var PDOStatement
43      */
44     protected $replace;
45
46     /**
47      * Prepared statement for selecting the definition of a given term
48      *
49      * @var PDOStatement
50      */
51     protected $defination;
52
53     /**
54      * Prepared statement for selecting the alias of a given definition
55      *
56      * @var PDOStatement
57      */
58     protected $alias;
59
60     /**
61      * Prepared statement for deleting the definition for a given term
62      *
63      * @var PDOStatement
64      */
65     protected $delete;
66
67     /**
68      * Creates the database if needed, connects to it, and sets up prepared
69      * statements for common operations.
70      *
71      * @return void
72      */
73     public function onInit()
74     {
75         try {
76             // Initialize the database connection
77             $this->db = new PDO('sqlite:' . $this->dir . 'lart.db');
78             if (!$this->db) {
79                 return;
80             }
81
82             // Check to see if the table exists
83             $table = $this->db->query('SELECT COUNT(*) FROM sqlite_master WHERE name = ' . $this->db->quote('lart'))->fetchColumn();
84
85             // Create database tables if necessary
86             if (!$table) {
87                 $this->debug('Creating the database schema');
88                 $this->db->exec('
89                     CREATE TABLE lart (name VARCHAR(255), definition TEXT, hostmask VARCHAR(50), tstamp VARCHAR(19));
90                     CREATE UNIQUE INDEX lart_name ON lart (name)
91                 ');
92             }
93             unset($table);
94
95             // Get the list of columns from the table
96             $table = $this->db->query('PRAGMA table_info(' . $this->db->quote('lart') . ')')->fetchAll();
97
98             $columns = array();
99             foreach($table as $key => $column) {
100                 $columns[] = trim(strtolower($column['name']));
101             }
102             unset($table);
103
104             // Update table as neccessary
105             if (!in_array('hostmask', $columns)) {
106                 $this->debug('Updating the database schema');
107                 $this->db->exec('
108                     BEGIN TRANSACTION;
109                     CREATE TEMPORARY TABLE lart_backup (name VARCHAR(255), definition TEXT);
110                     INSERT INTO lart_backup SELECT name, definition FROM lart;
111                     DROP TABLE lart;
112                     CREATE TABLE lart (name VARCHAR(255), definition TEXT, hostmask VARCHAR(50), tstamp VARCHAR(19));
113                     INSERT INTO lart SELECT name, definition, NULL, NULL FROM lart_backup;
114                     DROP TABLE lart_backup;
115                     COMMIT;
116                     CREATE UNIQUE INDEX lart_name ON lart (name)
117                 ');
118             }
119             unset($table, $key, $column, $columns);
120
121             // Initialize prepared statements for common operations
122             $this->replace = $this->db->prepare('
123                 REPLACE INTO lart (name, definition, hostmask, tstamp) VALUES (:name, :definition, :hostmask, :tstamp)
124             ');
125             $this->defination = $this->db->prepare('
126                 SELECT definition FROM lart WHERE LOWER(name) = LOWER(:name)
127             ');
128             $this->alias = $this->db->prepare('
129                 SELECT name FROM lart WHERE LOWER(definition) = LOWER(:definition)
130             ');
131             $this->select = $this->db->prepare('
132                 SELECT * FROM lart WHERE LOWER(name) = LOWER(:name)
133             ');
134             $this->delete = $this->db->prepare('
135                 DELETE FROM lart WHERE LOWER(name) = LOWER(:name)
136             ');
137         } catch (PDOException $e) { }
138     }
139
140     /**
141      * Returns whether or not the plugin's dependencies are met.
142      *
143      * @param Phergie_Driver_Abstract $client Client instance
144      * @param array $plugins List of short names for plugins that the
145      *                       bootstrap file intends to instantiate
146      * @see Phergie_Plugin_Abstract_Base::checkDependencies()
147      * @return bool TRUE if dependencies are met, FALSE otherwise
148      */
149     public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
150     {
151         $errors = array();
152
153         if (!extension_loaded('PDO')) {
154             $errors[] = 'PDO php extension is required';
155         }
156         if (!extension_loaded('pdo_sqlite')) {
157             $errors[] = 'pdo_sqlite php extension is required';
158         }
159
160         return empty($errors) ? true : $errors;
161     }
162
163     /**
164      * Retrieves the definition for a given term if it exists, from the cache
165      * or from the database into the cache if needed, and returns it.
166      *
167      * @param string $term Term to search for
168      * @return mixed String containing the definition or FALSE if no definition
169      *               exists
170      */
171     protected function getDefinition($term)
172     {
173         if (!isset($this->cache[trim(strtolower($term))])) {
174             $this->defination->execute(array(':name' => trim($term)));
175             $definition = $this->defination->fetchColumn();
176             if ($definition) {
177                 $this->cache[trim(strtolower($term))] = $definition;
178             } else {
179                 return false;
180             }
181         }
182         return $this->cache[trim(strtolower($term))];
183     }
184
185     /**
186      * Retrieves the alias for a given definition if it exists from the database
187      * and returns it otherwise return FALSE.
188      *
189      * @param string $term Term to search for
190      * @return mixed String containing the definition or FALSE if no alias exist
191      */
192     protected function getAlias($definition)
193     {
194         $this->alias->execute(array(':definition' => trim($definition)));
195         $names = $this->alias->fetchAll();
196         if ($names && count($names) > 0) {
197             return $names;
198         } else {
199             return false;
200         }
201     }
202
203     /**
204      * Searches for a definition for a given term, resolves definition aliases,
205      * handles cases of circular references, and sends the definition back to
206      * the sender if it exists.
207      *
208      * @param string $message Message containing the term to search for
209      * @return void
210      */
211     private function checkLart($message)
212     {
213         if (empty($message) || strlen($message) > 255) {
214             return;
215         }
216
217         $definition = trim($this->getDefinition($message));
218         $seen = array(trim(strtolower($message)));
219
220         if (!empty($definition)) {
221             do {
222                 if (trim(strtolower($message)) == trim(strtolower($definition))) {
223                     break;
224                 }
225                 $redirect = trim($this->getDefinition($definition));
226                 if (!empty($redirect)) {
227                     if (strtolower($definition) == strtolower($redirect)) {
228                         $definition = $redirect;
229                         break;
230                     }
231                     $seen[] = strtolower($definition);
232                     if (in_array($redirect, $seen)) {
233                         $this->debug('Alias redirection loop detected');
234                         $this->deleteLart($message);
235                         $mod = $this->getIni('gender') == 'F' ? 'her' : 'his';
236                         $this->doAction(
237                             $this->event->getSource(),
238                             'puts ' . $mod . ' hands over ' . $mod . ' ears and cries, "Stop confusing me!"'
239                         );
240                         return;
241                     }
242                     $definition = $redirect;
243                 }
244             } while(!empty($redirect));
245
246             if (substr(strtolower($definition), 0, 3) == '/me') {
247                 $definition = trim(substr($definition, 3));
248                 $this->doAction($this->event->getSource(), $definition);
249             } else {
250                 $this->doPrivmsg($this->event->getSource(), $definition);
251             }
252         }
253     }
254
255     /**
256      * Recursively gathers an array of aliases linking to the given term and
257      * for each term of that alias
258      *
259      * @param string $term Term to search for
260      * @return array An array of aliases for the given term
261      */
262     public function getAliases($term, $recursive = true, $aliasCache = array())
263     {
264         if (!isset($aliasCache) || !is_array($aliasCache)) {
265             $aliasCache = array();
266         }
267
268         $alias = $this->getAlias($term);
269         if (is_array($alias)) {
270             foreach($alias as $key => $value) {
271                 $name = $value['name'];
272                 if (empty($name)) continue;
273                 if (!in_array($name, $aliasCache)) {
274                     $aliasCache[] = $name;
275                     if ($recursive) {
276                         $aliasCache = $this->getAliases($name, $recursive, $aliasCache);
277                     }
278                 }
279             }
280         }
281         return $aliasCache;
282     }
283
284     /**
285      * Deletes the given lart and optionally delete all aliases for that lart
286      * recursively.
287      *
288      * @param string $name Term to search for
289      * @param bool $recursive Whethere or not to recursively delete aliases
290      * @return void
291      */
292     function deleteLart($name, $recursive = true)
293     {
294         if (!$recursive) {
295             $this->debug('Removing term: ' . $name);
296             $this->delete->execute(array(':name' => $name));
297             unset($this->cache[trim(strtolower($name))]);
298         } else {
299             $aliases = $this->getAliases($name);
300             $aliases[] = $name;
301             $aliases = array_unique($aliases);
302
303             $this->debug('Removing terms: ' . implode(', ', $aliases));
304             foreach($aliases as $key => $alias) {
305                 $this->delete->execute(array(':name' => $alias));
306                 unset($this->cache[trim(strtolower($alias))]);
307             }
308         }
309     }
310
311     /**
312      * Performs a lookup for the contents of messages to the bot or a channel
313      * in which the bot is present.
314      *
315      * @return void
316      */
317     public function onPrivmsg()
318     {
319         if (!$this->db) {
320             return;
321         }
322
323         $source = $this->event->getSource();
324         $message = $this->event->getArgument(1);
325         $target = $this->event->getNick();
326         $hostmask = $this->event->getHostmask();
327         $timenow = time();
328         $today = date('md');
329
330         // Command prefix check
331         $prefix = preg_quote(trim($this->getIni('command_prefix')));
332         $bot = preg_quote($this->getIni('nick'));
333         $exp = '(?:(?:' . $bot . '\s*[:,>]?\s+(?:' . $prefix . ')?)|(?:' . $prefix . '))';
334
335         if ($this->flushed != $today) {
336             unset($this->cache);
337             $this->flushed = $today;
338             $this->cache = array();
339         }
340
341         $adminOnly = $this->getPluginIni('admin_only');
342         if (preg_match('/^' . $exp . 'lartinfo\s+(.*?)$/i', $message, $match)) {
343             if ($this->fromAdmin()) {
344                 $name = trim($match[1]);
345                 if (!empty($name)) {
346                     $this->select->execute(array(':name' => $name));
347                     $info = $this->select->fetch();
348                     if ($info && count($info) > 0) {
349                         $name = $info['name'];
350                         $desc = $info['definition'];
351                         $host = (isset($info['hostmask']) ? substr($info['hostmask'], 0, strpos($info['hostmask'], '!')) : 'N/A');
352                         $time = (isset($info['tstamp']) ? $this->getCountdown(time() -$info['tstamp']) : 'N/A');
353                         $aliases = $this->getAliases($name);
354                         $aliases = (count($aliases) > 0 ? implode(', ', $aliases) : 'N/A');
355
356                         $this->doPrivmsg(
357                             $this->event->getSource(),
358                             $target . ': Lart Info -> Term: ' . $name . ' | Definition: ' . $desc . ' | User: ' . $host . ' | Added: ' . $time . ' ago | Aliases: ' . $aliases
359                         );
360                     } else {
361                         $this->doNotice($target, 'Unknown Lart: ' . $name);
362                     }
363                     unset($info);
364                 }
365             } else {
366                 $this->doNotice($target, 'You do not have permission to view the lart info.');
367             }
368         } else if (preg_match('/^(' . $bot . '\s*[:,>]?\s+)?(.*?)\s+is\s+(.*)$/i', $message, $match)) {
369             list(, $address, $name, $definition) = array_pad($match, 4, null);
370             if (!empty($name) && !empty($definition) && (empty($address) xor $source[0] == '#')) {
371                 if (!$adminOnly || $this->fromAdmin()) {
372                     $name = trim($name);
373                     $definition = trim($definition);
374
375                     if (!empty($name) && !empty($definition)) {
376                         $this->debug('Replacing term: ' . $name . ' = ' . $definition);
377                         $this->replace->execute(array(':name' => $name, ':definition' => $definition, ':hostmask' => $hostmask, ':tstamp' => $timenow));
378                         $this->cache[trim(strtolower($name))] = $definition;
379                         $this->doNotice($target, 'Added lart "' . $name . '".');
380                     }
381                 } else {
382                     $this->doNotice($target, 'You do not have permission to add larts.');
383                 }
384             }
385         } else if (preg_match('/^(' . $exp . ')?forget\s+(.*)$/i', $message, $match)) {
386             if (!$adminOnly || $this->fromAdmin()) {
387                 list(, $address, $name) = array_pad($match, 3, null);
388                 $name = trim($name);
389                 if (!empty($name) && (empty($address) xor $source[0] == '#')) {
390                     $defination = $this->getDefinition($name);
391                     if ($defination) {
392                         $this->deleteLart($name);
393                         $this->doNotice($target, 'Removed lart "' . $name . '" and all its aliases.');
394                     } else {
395                         $this->doNotice($target, 'Lart "' . $name . '" does not exist.');
396                     }
397                 }
398             } else {
399                 $this->doNotice($target, 'You do not have permission to remove larts.');
400             }
401         } else {
402             $this->checkLart($message);
403         }
404     }
405
406     /**
407      * Performs a lookup for the contents of CTCP ACTION (/me) commands.
408      *
409      * @return void
410      */
411     public function onAction()
412     {
413         if (!$this->db) {
414             return;
415         }
416
417         $this->checkLart('/me ' . $this->event->getArgument(1));
418     }
419 }
420
Note: See TracBrowser for help on using the browser.