Assembla home | Assembla project page
 

root/trunk/Phergie/Plugin/Karma.php

Revision 273, 16.6 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  * Handles requests for incrementation or decrementation of a maintained list
5  * of counters for specified terms and antithrottling to prevent extreme
6  * inflation or depression of counters by any single individual.
7  */
8 class Phergie_Plugin_Karma 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      * Stores the SQLite object
19      *
20      * @var resource
21      */
22     protected $db = null;
23
24     /**
25      * Retains the last garbage collection date
26      *
27      * @var array
28      */
29     protected $lastGc = null;
30
31     /**
32      * Logs the karma usages and limits users to one karma change per word
33      * and per day
34      *
35      * @return void
36      */
37     protected $log = array();
38
39     /**
40      * Some fixed karma values, keys must be lowercase
41      *
42      * @var array
43      */
44     protected $fixedKarma;
45
46     /**
47      * A list of blacklisted values
48      *
49      * @var array
50      */
51     protected $karmaBlacklist;
52
53     /**
54      * Answers for correct assertions
55      */
56     protected $positiveAnswers;
57
58     /**
59      * Answers for incorrect assertions
60      */
61     protected $negativeAnswers;
62
63     /**
64      * Prepared PDO statements
65      *
66      * @var PDOStatement
67      */
68     protected $insertKarma;
69     protected $updateKarma;
70     protected $fetchKarma;
71     protected $insertComment;
72
73     /**
74      * Connects to the database containing karma ratings and initializes
75      * class properties.
76      *
77      * @return void
78      */
79     public function onInit()
80     {
81         $this->db = null;
82         $this->lastGc = null;
83         $this->log = array();
84
85         if(!defined('M_EULER')) {
86             define('M_EULER', '0.57721566490153286061');
87         }
88
89         $this->fixedKarma = array(
90             'phergie'      => '%s has karma of awesome',
91             'pi'           => '%s has karma of ' . M_PI,
92             'Π'            => '%s has karma of ' . M_PI,
93             'π'            => '%s has karma of ' . M_PI,
94             'chucknorris'  => '%s has karma of Warning: Integer out of range',
95             'chuck norris' => '%s has karma of Warning: Integer out of range',
96             'c'            => '%s has karma of 299 792 458 m/s',
97             'e'            => '%s has karma of ' . M_E,
98             'euler'        => '%s has karma of ' . M_EULER,
99             'mole'         => '%s has karma of 6.02214e23 molecules',
100             'avogadro'     => '%s has karma of 6.02214e23 molecules',
101             'spoon'        => '%s has no karma. There is no spoon',
102             'mc^2'         => '%s has karma of e',
103             'mc2'          => '%s has karma of e',
104             'mc²'          => '%s has karma of e',
105             'i'            => '%s haz big karma',
106             'karma' => 'The karma law says that all living creatures are responsible for their karma - their actions and the effects of their actions. You should watch yours.'
107         );
108
109         $this->karmaBlacklist = array(
110             '*',
111             'all',
112             'everything'
113         );
114
115         $this->positiveAnswers = array(
116             'No kidding, %owner% totally kicks %owned%\'s ass !',
117             'True that.',
118             'I concur.',
119             'Yay, %owner% ftw !',
120             '%owner% is made of WIN!',
121             'Nothing can beat %owner%!',
122         );
123
124         $this->negativeAnswers = array(
125             'No sir, not at all.',
126             'You\'re wrong dude, %owner% wins.',
127             'I\'d say %owner% is better than %owned%.',
128             'You must be joking, %owner% ftw!',
129             '%owned% is made of LOSE!',
130             '%owned% = Epic Fail',
131         );
132
133         $static = $this->getPluginIni('static');
134         if ($static) {
135             $this->fixedKarma[strtolower($this->getIni('nick'))] = $static;
136         }
137
138         try {
139             // Load or initialize the database
140             $this->db = new PDO('sqlite:' . $this->dir . 'karma.db');
141             if (!$this->db) {
142                 return;
143             }
144
145             // Check to see if the table exists
146             $table = $this->db->query('SELECT COUNT(*) FROM sqlite_master WHERE name = ' . $this->db->quote('karmas'))->fetchColumn();
147
148             // Create database tables if necessary
149             if (!$table) {
150                 $this->debug('Creating the database schema');
151                 $this->db->query('
152                     CREATE TABLE karmas ( word VARCHAR ( 255 ), karma MEDIUMINT ) ;
153                     CREATE UNIQUE INDEX word ON karmas ( word ) ;
154                     CREATE INDEX karmaIndex ON karmas ( karma ) ;
155                 ');
156                 $this->db->query('
157                     CREATE TABLE comments ( wordid INT , comment VARCHAR ( 255 ) ) ;
158                     CREATE INDEX wordidIndex ON comments ( wordid ) ;
159                     CREATE UNIQUE INDEX commentUnique ON comments ( comment ) ;
160                 ');
161             }
162
163             $this->insertKarma = $this->db->prepare('
164                 INSERT INTO karmas (
165                     word,
166                     karma
167                 )
168                 VALUES (
169                     :word,
170                     :karma
171                 )
172             ');
173
174             $this->insertComment = $this->db->prepare('
175                 INSERT INTO comments (
176                     wordid,
177                     comment
178                 )
179                 VALUES (
180                     :wordid,
181                     :comment
182                 )
183             ');
184
185             $this->fetchKarma = $this->db->prepare('
186                 SELECT karma, ROWID id FROM karmas WHERE LOWER(word) = LOWER(:word) LIMIT 1
187             ');
188
189             $this->updateKarma = $this->db->prepare('
190                 UPDATE karmas SET karma = :karma WHERE LOWER(word) = LOWER(:word)
191             ');
192         } catch (PDOException $e) { }
193     }
194
195     /**
196      * Returns whether or not the plugin's dependencies are met.
197      *
198      * @param Phergie_Driver_Abstract $client Client instance
199      * @param array $plugins List of short names for plugins that the
200      *                       bootstrap file intends to instantiate
201      * @see Phergie_Plugin_Abstract_Base::checkDependencies()
202      * @return bool TRUE if dependencies are met, FALSE otherwise
203      */
204     public static function checkDependencies(Phergie_Driver_Abstract $client, array $plugins)
205     {
206         $errors = array();
207
208         if (!extension_loaded('PDO')) {
209             $errors[] = 'PDO php extension is required';
210         }
211         if (!extension_loaded('pdo_sqlite')) {
212             $errors[] = 'pdo_sqlite php extension is required';
213         }
214
215         return empty($errors) ? true : $errors;
216     }
217
218     /**
219      * Handles requests for incrementation, decrementation, or lookup of karma
220      * ratings sent via messages from users.
221      *
222      * @return void
223      */
224     public function onPrivmsg()
225     {
226         if (!$this->db) {
227             return;
228         }
229
230         $source = $this->event->getSource();
231         $message = $this->event->getArgument(1);
232         $target = $this->event->getNick();
233
234         // Command prefix check
235         $prefix = preg_quote(trim($this->getIni('command_prefix')));
236         $bot = preg_quote($this->getIni('nick'));
237         $exp = '(?:(?:' . $bot . '\s*[:,>]?\s+(?:' . $prefix . ')?)|(?:' . $prefix . '))';
238
239         // Karma status request
240         if (preg_match('#^' . $exp . 'karma\s+(.+)$#i', $message, $m)) {
241             // Return user's value if "me" is requested
242             if (strtolower($m[1]) === 'me') {
243                 $m[1] = $target;
244             }
245             // Clean the term
246             $term = $this->doCleanWord($m[1]);
247
248             // Check the blacklist
249             if (is_array($this->karmaBlacklist) && in_array($term, $this->karmaBlacklist)) {
250                 $this->doNotice($target, $term . ' is blacklisted');
251                 return;
252             }
253
254             // Return fixed value if set
255             if (isset($this->fixedKarma[$term])) {
256                 $this->doPrivmsg($source, $target . ': ' . sprintf($this->fixedKarma[$term], $m[1]) . '.');
257                 return;
258             }
259
260             // Return current karma or neutral if not set yet
261             $this->fetchKarma->execute(array(':word'=>$term));
262             $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
263
264             // Sanity check if someone if someone prefixed their conversation with karma
265             if (!$res && substr_count($term, ' ') > 1 && !(substr($m[1], 0, 1) === '(' && substr($m[1], -1) === ')')) {
266                 return;
267             }
268
269             // Clean the raw term if it was contained within brackets
270             if (substr($m[1], 0, 1) === '(' && substr($m[1], -1) === ')') {
271                 $m[1] = substr($m[1], 1, -1);
272             }
273
274             if ($res && $res['karma'] != 0) {
275                 $this->doPrivmsg($source, $target . ': ' . $m[1] . ' has karma of ' . $res['karma'] . '.');
276             } else {
277                 $this->doPrivmsg($source, $target . ': ' . $m[1] . ' has neutral karma.');
278             }
279         // Incrementation/decrementation request
280         } elseif (preg_match('{^' . $exp . '?(?:(\+{2,2}|-{2,2})(\S+?|\(.+?\)+)|(\S+?|\(.+?\)+)(\+{2,2}|-{2,2}))(?:\s+(.*))?$}ix', $message, $m)) {
281             if (!empty($m[4])) {
282                 $m[1] = $m[4]; // Increment/Decrement
283                 $m[2] = $m[3]; // Word
284             }
285             $m[3] = (isset($m[5]) ? $m[5] : null); // Comment
286             unset($m[4], $m[5]);
287             list(, $sign, $word, $comment) = array_pad($m, 4, null);
288
289             // Clean the word
290             $word = strtolower($this->doCleanWord($word));
291             if (empty($word)) {
292                 return;
293             }
294
295             // Do nothing if the karma is fixed or blacklisted
296             if (isset($this->fixedKarma[$word]) ||
297                 is_array($this->karmaBlacklist) && in_array($word, $this->karmaBlacklist)) {
298                 return;
299             }
300
301             // Force a decrementation if someone tries to update his own karma
302             if ($word == strtolower($target) && $sign != '--' && !$this->fromAdmin(true)) {
303                 $this->doNotice($target, 'Bad ' . $target . '! You can not modify your own Karma. Shame on you!');
304                 $sign = '--';
305             }
306
307             // Antithrottling check
308             $host = $this->event->getHost();
309             $limit = $this->getPluginIni('limit');
310             if ($limit > 0 && !$this->fromAdmin()) {
311                 if (isset($this->log[$host][$word]) && $this->log[$host][$word] >= $limit) {
312                     // Three strikes, you're out, so lets decrement their karma for spammage
313                     if ($this->log[$host][$word] == ($limit+3)) {
314                         $this->doNotice($target, 'Bad ' . $target . '! Didn\'t I tell you that you reached your limit already?');
315                         $this->log[$host][$word] = $limit;
316                         $word = $target;
317                         $sign = '--';
318                     // Toss a notice to the user if they reached their limit
319                     } else {
320                         $this->doNotice($target, 'You have currently reached your limit in modifying ' . $word . ' for this day, please wait a bit.');
321                         $this->log[$host][$word]++;
322                         return;
323                     }
324                 } else {
325                     if (isset($this->log[$host][$word])) {
326                         $this->log[$host][$word]++;
327                     } else {
328                         $this->log[$host][$word] = 1;
329                     }
330                 }
331             }
332
333             // Get the current value then update or create entry
334             $this->fetchKarma->execute(array(':word'=>$word));
335             $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
336             if ($res) {
337                 $karma = ($res['karma'] + ($sign == '++' ? 1 : -1));
338                 $args = array(
339                     ':word' => $word,
340                     ':karma' => $karma
341                 );
342                 $this->debug('Updated karma: ' . $word . ' (Sign: ' . substr($sign, 1) . ') = ' . $karma);
343                 $this->updateKarma->execute($args);
344             } else {
345                 $karma = ($sign == '++' ? '1' : '-1');
346                 $args = array(
347                     ':word' => $word,
348                     ':karma' => $karma
349                 );
350                 $this->debug('Inserted karma: ' . $word . ' (Sign: ' . substr($sign, 1) . ') = ' . $karma);
351                 $this->insertKarma->execute($args);
352                 $this->fetchKarma->execute(array(':word'=>$word));
353                 $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
354             }
355             $id = $res['id'];
356             // Add comment
357             $comment = preg_replace('{(?:^//(.*)|^#(.*)|^/\*(.*?)\*/$)}', '$1$2$3', $comment);
358             if (!empty($comment)) {
359                 $this->debug('Inserted comment: ' . $comment);
360                 $this->insertComment->execute(array(':wordid' => $id, ':comment' => $comment));
361             }
362             // Perform garbage collection on the antithrottling log if needed
363             if (date('d') !== $this->lastGc) {
364                 $this->doGc();
365             }
366         // Assertion request
367         } elseif (preg_match('#^' . $exp . '?([^><]+)(<|>)([^><]+)$#', $message, $m)) {
368             // Trim words
369             $word1 = strtolower($this->doCleanWord($m[1]));
370             $word2 = strtolower($this->doCleanWord($m[3]));
371             $operator = $m[2];
372
373             // Do nothing if the karma is fixed
374             if (isset($this->fixedKarma[$word1]) || isset($this->fixedKarma[$word2]) ||
375                 empty($word1) || empty($word2)) {
376                 return;
377             }
378
379             // Fetch first word
380             if ($word1 === '*' || $word1 === 'all' || $word1 === 'everything') {
381                 $res = array('karma' => 0);
382                 $word1 = 'everything';
383             } else {
384                 $this->fetchKarma->execute(array(':word'=>$word1));
385                 $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
386             }
387             // If it exists, fetch second word
388             if ($res) {
389                 if ($word2 === '*' || $word2 === 'all' || $word2 === 'everything') {
390                     $res2 = array('karma' => 0);
391                     $word2 = 'everything';
392                 } else {
393                     $this->fetchKarma->execute(array(':word'=>$word2));
394                     $res2 = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
395                 }
396                 // If it exists, compare and return value
397                 if ($res2 && $res['karma'] != $res2['karma']) {
398                     $assertion = ($operator === '<' && $res['karma'] < $res2['karma']) || ($operator === '>' && $res['karma'] > $res2['karma']);
399                     // Switch arguments if they are in the wrong order
400                     if ($operator === '<') {
401                         $tmp = $word2;
402                         $word2 = $word1;
403                         $word1 = $tmp;
404                     }
405                     $this->doPrivmsg($source, $assertion ? $this->fetchPositiveAnswer($word1, $word2) : $this->fetchNegativeAnswer($word1, $word2));
406                     // If someone asserts that something is greater or lesser than everything, we increment/decrement that something at the same time
407                     if ($word2 === 'everything') {
408                         $this->event = clone$this->event;
409                         $this->event->setArguments(array($this->event->getArgument(0), '++'.$word1));
410                         $this->onPrivmsg();
411                     } elseif ($word1 === 'everything') {
412                         $this->event = clone$this->event;
413                         $this->event->setArguments(array($this->event->getArgument(0), '--'.$word2));
414                         $this->onPrivmsg();
415                     }
416                 }
417             }
418         }
419     }
420
421     protected function fetchPositiveAnswer($owner, $owned)
422     {
423         return str_replace(array('%owner%','%owned%'), array($owner, $owned), $this->positiveAnswers[array_rand($this->positiveAnswers,1)]);
424     }
425
426     protected function fetchNegativeAnswer($owned, $owner)
427     {
428         return str_replace(array('%owner%','%owned%'), array($owner, $owned), $this->negativeAnswers[array_rand($this->negativeAnswers,1)]);
429     }
430
431     protected function doCleanWord($word)
432     {
433         $word = trim($word);
434         if (substr($word, 0, 1) === '(' && substr($word, -1) === ')') {
435             $word = trim(substr($word, 1, -1));
436         }
437         $word = preg_replace('#\s+#', ' ', strtolower(trim($word)));
438         return $word;