/**
* Handles requests for incrementation or decrementation of a maintained list
- * of counters for specified terms.
+ * of counters for specified terms and antithrottling to prevent extreme
+ * inflation or depression of counters by any single individual.
*
* @category Phergie
* @package Phergie_Plugin_Karma
* @author Phergie Development Team <team@phergie.org>
* @license http://phergie.org/license New BSD License
* @link http://pear.phergie.org/package/Phergie_Plugin_Karma
- * @uses extension PDO
- * @uses extension pdo_sqlite
- * @uses Phergie_Plugin_Command pear.phergie.org
- * @uses Phergie_Plugin_Message pear.phergie.org
*/
class Phergie_Plugin_Karma extends Phergie_Plugin_Abstract
{
/**
- * SQLite object
+ * Stores the SQLite object
*
* @var resource
*/
protected $db = null;
/**
- * Prepared statement to add a new karma record
+ * Retains the last garbage collection date
*
- * @var PDOStatement
+ * @var array
*/
- protected $insertKarma;
+ protected $lastGc = null;
/**
- * Prepared statement to update an existing karma record
+ * Logs the karma usages and limits users to one karma change per word
+ * and per day
*
- * @var PDOStatement
+ * @return void
*/
- protected $updateKarma;
+ protected $log = array();
/**
- * Retrieves an existing karma record
+ * Some fixed karma values, keys must be lowercase
*
- * @var PDOStatement
+ * @var array
*/
- protected $fetchKarma;
+ protected $fixedKarma;
/**
- * Retrieves an existing fixed karma record
+ * A list of blacklisted values
*
- * @var PDOStatement
+ * @var array
*/
- protected $fetchFixedKarma;
+ protected $karmaBlacklist;
/**
- * Retrieves a positive answer for a karma comparison
- *
- * @var PDOStatement
+ * Answers for correct assertions
+ */
+ protected $positiveAnswers;
+
+ /**
+ * Answers for incorrect assertions
*/
- protected $fetchPositiveAnswer;
+ protected $negativeAnswers;
/**
- * Retrieves a negative answer for a karma comparison
+ * Prepared PDO statements
*
* @var PDOStatement
*/
- protected $fetchNegativeAnswer;
+ protected $insertKarma;
+ protected $updateKarma;
+ protected $fetchKarma;
+ protected $insertComment;
/**
- * Check for dependencies and initializes a database connection and
- * prepared statements.
+ * Connects to the database containing karma ratings and initializes
+ * class properties.
*
* @return void
*/
public function onLoad()
{
- $plugins = $this->getPluginHandler();
- $plugins->getPlugin('Command');
- $plugins->getPlugin('Message');
+ $this->db = null;
+ $this->lastGc = null;
+ $this->log = array();
- $file = dirname(__FILE__) . '/Karma/karma.db';
- $this->db = new PDO('sqlite:' . $file);
+ if(!defined('M_EULER')) {
+ define('M_EULER', '0.57721566490153286061');
+ }
- $this->fetchKarma = $this->db->prepare('
- SELECT karma
- FROM karmas
- WHERE term = :term
- LIMIT 1
- ');
+ $this->fixedKarma = array(
+ 'phergie' => '%s has karma of awesome',
+ 'pi' => '%s has karma of ' . M_PI,
+ 'Î ' => '%s has karma of ' . M_PI,
+ 'Ï€' => '%s has karma of ' . M_PI,
+ 'chucknorris' => '%s has karma of Warning: Integer out of range',
+ 'chuck norris' => '%s has karma of Warning: Integer out of range',
+ 'c' => '%s has karma of 299 792 458 m/s',
+ 'e' => '%s has karma of ' . M_E,
+ 'euler' => '%s has karma of ' . M_EULER,
+ 'mole' => '%s has karma of 6.02214e23 molecules',
+ 'avogadro' => '%s has karma of 6.02214e23 molecules',
+ 'spoon' => '%s has no karma. There is no spoon',
+ 'mc^2' => '%s has karma of E',
+ 'mc2' => '%s has karma of E',
+ 'mc²' => '%s has karma of E',
+ 'i' => '%s haz big karma',
+ '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.'
+ );
- $this->insertKarma = $this->db->prepare('
- INSERT INTO karmas (term, karma)
- VALUES (:term, :karma)
- ');
+ $this->karmaBlacklist = array(
+ '*',
+ 'all',
+ 'everything'
+ );
- $this->updateKarma = $this->db->prepare('
- UPDATE karmas
- SET karma = :karma
- WHERE term = :term
+ $this->positiveAnswers = array(
+ 'No kidding, %owner% totally kicks %owned%\'s ass !',
+ 'True that.',
+ 'I concur.',
+ 'Yay, %owner% ftw !',
+ '%owner% is made of WIN!',
+ 'Nothing can beat %owner%!',
+ );
+
+ $this->negativeAnswers = array(
+ 'No sir, not at all.',
+ 'You\'re wrong dude, %owner% wins.',
+ 'I\'d say %owner% is better than %owned%.',
+ 'You must be joking, %owner% ftw!',
+ '%owned% is made of LOSE!',
+ '%owned% = Epic Fail',
+ );
+
+ // Load or initialize the database
+ $class = new ReflectionClass(get_class($this));
+ $dir = dirname($class->getFileName() . '/' . $this->name);
+ $this->db = new PDO('sqlite:' . $dir . 'karma.db');
+
+ // Check to see if the table exists
+ $table = $this->db->query('
+ SELECT COUNT(*)
+ FROM sqlite_master
+ WHERE name = ' . $this->db->quote('karmas')
+ )->fetchColumn();
+
+ // Create database tables if necessary
+ if (!$table) {
+ $this->db->query('
+ CREATE TABLE karmas ( word VARCHAR ( 255 ), karma MEDIUMINT );
+ CREATE UNIQUE INDEX word ON karmas ( word );
+ CREATE INDEX karmaIndex ON karmas ( karma );
+ CREATE TABLE comments ( wordid INT , comment VARCHAR ( 255 ) );
+ CREATE INDEX wordidIndex ON comments ( wordid );
+ CREATE UNIQUE INDEX commentUnique ON comments ( comment );
+ ');
+ }
+
+ $this->insertKarma = $this->db->prepare('
+ INSERT INTO karmas (
+ word,
+ karma
+ )
+ VALUES (
+ :word,
+ :karma
+ )
');
- $this->fetchFixedKarma = $this->db->prepare('
- SELECT karma
- FROM fixed_karmas
- WHERE term = :term
- LIMIT 1
+ $this->insertComment = $this->db->prepare('
+ INSERT INTO comments (
+ wordid,
+ comment
+ )
+ VALUES (
+ :wordid,
+ :comment
+ )
');
- $this->fetchPositiveAnswer = $this->db->prepare('
- SELECT answer
- FROM positive_answers
- ORDER BY RANDOM()
- LIMIT 1
+ $this->fetchKarma = $this->db->prepare('
+ SELECT karma, ROWID id FROM karmas WHERE LOWER(word) = LOWER(:word) LIMIT 1
');
- $this->fetchNegativeAnswer = $this->db->prepare('
- SELECT answer
- FROM negative_answers
- ORDER BY RANDOM()
- LIMIT 1
+ $this->updateKarma = $this->db->prepare('
+ UPDATE karmas SET karma = :karma WHERE LOWER(word) = LOWER(:word)
');
}
/**
- * Get the canonical form of a given term.
+ * Checks for dependencies.
*
- * In the canonical form all sequences of whitespace
- * are replaced by a single space and all characters
- * are lowercased.
- *
- * @param string $term Term for which a canonical form is required
- *
- * @return string Canonical term
+ * @return void
*/
- protected function getCanonicalTerm($term)
+ public static function onLoad()
{
- $canonicalTerm = strtolower(preg_replace('|\s+|', ' ', trim($term, '()')));
- switch ($canonicalTerm) {
- case 'me':
- $canonicalTerm = strtolower($this->event->getNick());
- break;
- case 'all':
- case '*':
- case 'everything':
- $canonicalTerm = 'everything';
- break;
- }
- return $canonicalTerm;
+ if (!extension_loaded('PDO') || !extension_loaded('pdo_sqlite')) {
+ $this->fail('PDO and pdo_sqlite extensions must be installed');
+ }
}
/**
- * Intercepts a message and processes any contained recognized commands.
+ * Handles requests for incrementation, decrementation, or lookup of karma
+ * ratings sent via messages from users.
*
* @return void
*/
public function onPrivmsg()
{
- $message = $this->getEvent()->getText();
-
- $termPattern = '\S+?|\([^<>]+?\)+';
- $actionPattern = '(?P<action>\+\+|--)';
-
- $modifyPattern = <<<REGEX
- {^
- (?J) # allow overwriting capture names
- \s* # ignore leading whitespace
-
- (?: # start with ++ or -- before the term
- $actionPattern
- (?P<term>$termPattern)
- | # follow the term with ++ or --
- (?P<term>$termPattern)
- $actionPattern # allow no whitespace between the term and the action
- )
- $}ix
-REGEX;
-
- $versusPattern = <<<REGEX
- {^
- (?P<term0>$termPattern)
- \s+(?P<method><|>)\s+
- (?P<term1>$termPattern)$#
- $}ix
-REGEX;
-
- $match = null;
-
- if (preg_match($modifyPattern, $message, $match)) {
- $action = $match['action'];
- $term = $this->getCanonicalTerm($match['term']);
- $this->modifyKarma($term, $action);
- } elseif (preg_match($versusPattern, $message, $match)) {
- $term0 = trim($match['term0']);
- $term1 = trim($match['term1']);
- $method = $match['method'];
- $this->compareKarma($term0, $term1, $method);
+ $source = $this->event->getSource();
+ $message = $this->event->getArgument(1);
+ $target = $this->event->getNick();
+
+ // Command prefix check
+ $prefix = preg_quote(trim($this->getConfig('command.prefix')));
+ $bot = preg_quote($this->getConfig('connections.nick'));
+ $exp = '(?:(?:' . $bot . '\s*[:,>]?\s+(?:' . $prefix . ')?)|(?:' . $prefix . '))';
+
+ // Karma status request
+ if (preg_match('#^' . $exp . 'karma\s+(.+)$#i', $message, $m)) {
+ // Return user's value if "me" is requested
+ if (strtolower($m[1]) === 'me') {
+ $m[1] = $target;
+ }
+ // Clean the term
+ $term = $this->doCleanWord($m[1]);
+
+ // Check the blacklist
+ if (is_array($this->karmaBlacklist) && in_array($term, $this->karmaBlacklist)) {
+ $this->doNotice($target, $term . ' is blacklisted');
+ return;
+ }
+
+ // Return fixed value if set
+ if (isset($this->fixedKarma[$term])) {
+ $this->doPrivmsg($source, $target . ': ' . sprintf($this->fixedKarma[$term], $m[1]) . '.');
+ return;
+ }
+
+ // Return current karma or neutral if not set yet
+ $this->fetchKarma->execute(array(':word'=>$term));
+ $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
+
+ // Sanity check if someone if someone prefixed their conversation with karma
+ if (!$res && substr_count($term, ' ') > 1 && !(substr($m[1], 0, 1) === '(' && substr($m[1], -1) === ')')) {
+ return;
+ }
+
+ // Clean the raw term if it was contained within brackets
+ if (substr($m[1], 0, 1) === '(' && substr($m[1], -1) === ')') {
+ $m[1] = substr($m[1], 1, -1);
+ }
+
+ if ($res && $res['karma'] != 0) {
+ $this->doPrivmsg($source, $target . ': ' . $m[1] . ' has karma of ' . $res['karma'] . '.');
+ } else {
+ $this->doPrivmsg($source, $target . ': ' . $m[1] . ' has neutral karma.');
+ }
+ // Incrementation/decrementation request
+ } elseif (preg_match('{^' . $exp . '?(?:(\+{2,2}|-{2,2})(\S+?|\(.+?\)+)|(\S+?|\(.+?\)+)(\+{2,2}|-{2,2}))(?:\s+(.*))?$}ix', $message, $m)) {
+ if (!empty($m[4])) {
+ $m[1] = $m[4]; // Increment/Decrement
+ $m[2] = $m[3]; // Word
+ }
+ $m[3] = (isset($m[5]) ? $m[5] : null); // Comment
+ unset($m[4], $m[5]);
+ list(, $sign, $word, $comment) = array_pad($m, 4, null);
+
+ // Clean the word
+ $word = strtolower($this->doCleanWord($word));
+ if (empty($word)) {
+ return;
+ }
+
+ // Do nothing if the karma is fixed or blacklisted
+ if (isset($this->fixedKarma[$word]) ||
+ is_array($this->karmaBlacklist) && in_array($word, $this->karmaBlacklist)) {
+ return;
+ }
+
+ // Force a decrementation if someone tries to update his own karma
+ if ($word == strtolower($target) && $sign != '--' && !$this->fromAdmin(true)) {
+ $this->doNotice($target, 'Bad ' . $target . '! You can not modify your own Karma. Shame on you!');
+ $sign = '--';
+ }
+
+ // Antithrottling check
+ $host = $this->event->getHost();
+ $limit = $this->getConfig('karma.limit');
+ // This is waiting on the Acl plugin from Elazar, being bypassed for now
+ //if ($limit > 0 && !$this->fromAdmin()) {
+ if ($limit > 0) {
+ if (isset($this->log[$host][$word]) && $this->log[$host][$word] >= $limit) {
+ // Three strikes, you're out, so lets decrement their karma for spammage
+ if ($this->log[$host][$word] == ($limit+3)) {
+ $this->doNotice($target, 'Bad ' . $target . '! Didn\'t I tell you that you reached your limit already?');
+ $this->log[$host][$word] = $limit;
+ $word = $target;
+ $sign = '--';
+ // Toss a notice to the user if they reached their limit
+ } else {
+ $this->doNotice($target, 'You have currently reached your limit in modifying ' . $word . ' for this day, please wait a bit.');
+ $this->log[$host][$word]++;
+ return;
+ }
+ } else {
+ if (isset($this->log[$host][$word])) {
+ $this->log[$host][$word]++;
+ } else {
+ $this->log[$host][$word] = 1;
+ }
+ }
+ }
+
+ // Get the current value then update or create entry
+ $this->fetchKarma->execute(array(':word'=>$word));
+ $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
+ if ($res) {
+ $karma = ($res['karma'] + ($sign == '++' ? 1 : -1));
+ $args = array(
+ ':word' => $word,
+ ':karma' => $karma
+ );
+ $this->updateKarma->execute($args);
+ } else {
+ $karma = ($sign == '++' ? '1' : '-1');
+ $args = array(
+ ':word' => $word,
+ ':karma' => $karma
+ );
+ $this->insertKarma->execute($args);
+ $this->fetchKarma->execute(array(':word'=>$word));
+ $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
+ }
+ $id = $res['id'];
+ // Add comment
+ $comment = preg_replace('{(?:^//(.*)|^#(.*)|^/\*(.*?)\*/$)}', '$1$2$3', $comment);
+ if (!empty($comment)) {
+ $this->insertComment->execute(array(':wordid' => $id, ':comment' => $comment));
+ }
+ // Perform garbage collection on the antithrottling log if needed
+ if (date('d') !== $this->lastGc) {
+ $this->doGc();
+ }
+ // Assertion request
+ } elseif (preg_match('#^' . $exp . '?([^><]+)(<|>)([^><]+)$#', $message, $m)) {
+ // Trim words
+ $word1 = strtolower($this->doCleanWord($m[1]));
+ $word2 = strtolower($this->doCleanWord($m[3]));
+ $operator = $m[2];
+
+ // Do nothing if the karma is fixed
+ if (isset($this->fixedKarma[$word1]) || isset($this->fixedKarma[$word2]) ||
+ empty($word1) || empty($word2)) {
+ return;
+ }
+
+ // Fetch first word
+ if ($word1 === '*' || $word1 === 'all' || $word1 === 'everything') {
+ $res = array('karma' => 0);
+ $word1 = 'everything';
+ } else {
+ $this->fetchKarma->execute(array(':word'=>$word1));
+ $res = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
+ }
+ // If it exists, fetch second word
+ if ($res) {
+ if ($word2 === '*' || $word2 === 'all' || $word2 === 'everything') {
+ $res2 = array('karma' => 0);
+ $word2 = 'everything';
+ } else {
+ $this->fetchKarma->execute(array(':word'=>$word2));
+ $res2 = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
+ }
+ // If it exists, compare and return value
+ if ($res2 && $res['karma'] != $res2['karma']) {
+ $assertion = ($operator === '<' && $res['karma'] < $res2['karma']) || ($operator === '>' && $res['karma'] > $res2['karma']);
+ // Switch arguments if they are in the wrong order
+ if ($operator === '<') {
+ $tmp = $word2;
+ $word2 = $word1;
+ $word1 = $tmp;
+ }
+ $this->doPrivmsg($source, $assertion ? $this->fetchPositiveAnswer($word1, $word2) : $this->fetchNegativeAnswer($word1, $word2));
+ // If someone asserts that something is greater or lesser than everything, we increment/decrement that something at the same time
+ if ($word2 === 'everything') {
+ $this->event = clone$this->event;
+ $this->event->setArguments(array($this->event->getArgument(0), '++'.$word1));
+ $this->onPrivmsg();
+ } elseif ($word1 === 'everything') {
+ $this->event = clone$this->event;
+ $this->event->setArguments(array($this->event->getArgument(0), '--'.$word2));
+ $this->onPrivmsg();
+ }
+ }
+ }
}
}
- /**
- * Get the karma rating for a given term.
- *
- * @param string $term Term for which the karma rating needs to be
- * retrieved
- *
- * @return void
- */
- public function onCommandKarma($term)
+ protected function fetchPositiveAnswer($owner, $owned)
{
- $source = $this->getEvent()->getSource();
- $nick = $this->getEvent()->getNick();
-
- if (empty($term)) {
- return;
- }
-
- $canonicalTerm = $this->getCanonicalTerm($term);
-
- $fixedKarma = $this->fetchFixedKarma($canonicalTerm);
- if ($fixedKarma) {
- $message = $nick . ': ' . $term . $fixedKarma . '.';
- $this->doPrivmsg($source, $message);
- return;
- }
-
- $karma = $this->fetchKarma($canonicalTerm);
-
- $message = $nick . ': ';
-
- if ($term == 'me') {
- $message .= 'You have';
- } else {
- $message .= $term . ' has';
- }
-
- $message .= ' ';
-
- if ($karma) {
- $message .= 'karma of ' . $karma;
- } else {
- $message .= 'neutral karma';
- }
-
- $message .= '.';
-
- $this->doPrivmsg($source, $message);
+ return str_replace(array('%owner%','%owned%'), array($owner, $owned), $this->positiveAnswers[array_rand($this->positiveAnswers,1)]);
}
- /**
- * Resets the karma for a term to 0.
- *
- * @param string $term Term for which to reset the karma rating
- *
- * @return void
- */
- public function onCommandReincarnate($term)
+ protected function fetchNegativeAnswer($owned, $owner)
{
- $data = array(
- ':term' => $term,
- ':karma' => 0
- );
- $this->updateKarma->execute($data);
+ return str_replace(array('%owner%','%owned%'), array($owner, $owned), $this->negativeAnswers[array_rand($this->negativeAnswers,1)]);
}
- /**
- * Compares the karma between two terms. Optionally increases/decreases
- * the karma of either term.
- *
- * @param string $term0 First term
- * @param string $term1 Second term
- * @param string $method Comparison method (< or >)
- *
- * @return void
- */
- protected function compareKarma($term0, $term1, $method)
+ protected function doCleanWord($word)
{
- $event = $this->getEvent();
- $nick = $event->getNick();
- $source = $event->getSource();
-
- $canonicalTerm0 = $this->getCanonicalTerm($term0);
- $canonicalTerm1 = $this->getCanonicalTerm($term1);
-
- $fixedKarma0 = $this->fetchFixedKarma($canonicalTerm0);
- $fixedKarma1 = $this->fetchFixedKarma($canonicalTerm1);
-
- if ($fixedKarma0
- || $fixedKarma1
- || empty($canonicalTerm0)
- || empty($canonicalTerm1)
- ) {
- return;
- }
-
- if ($canonicalTerm0 == 'everything') {
- $change = $method == '<' ? '++' : '--';
- $this->modifyKarma($canonicalTerm1, $change);
- $karma0 = 0;
- $karma1 = $this->fetchKarma($canonicalTerm1);
- } elseif ($canonicalTerm1 == 'everything') {
- $change = $method == '<' ? '--' : '++';
- $this->modifyKarma($canonicalTerm0, $change);
- $karma0 = $this->fetchKarma($canonicalTerm1);
- $karma1 = 0;
- } else {
- $karma0 = $this->fetchKarma($canonicalTerm0);
- $karma1 = $this->fetchKarma($canonicalTerm1);
+ $word = trim($word);
+ if (substr($word, 0, 1) === '(' && substr($word, -1) === ')') {
+ $word = trim(substr($word, 1, -1));
}
-
- if (($method == '<'
- && $karma0 < $karma1)
- || ($method == '>'
- && $karma0 > $karma1)) {
- $replies = $this->fetchPositiveAnswer;
- } else {
- $replies = $this->fetchNegativeAnswer;
- }
- $replies->execute();
- $reply = $replies->fetchColumn();
-
- if (max($karma0, $karma1) == $karma1) {
- list($canonicalTerm0, $canonicalTerm1) =
- array($canonicalTerm1, $canonicalTerm0);
- }
-
- $message = str_replace(
- array('%owner%','%owned%'),
- array($canonicalTerm0, $canonicalTerm1),
- $reply
- );
-
- $this->doPrivmsg($source, $message);
+ $word = preg_replace('#\s+#', ' ', strtolower(trim($word)));
+ return $word;
}
/**
- * Modifes a term's karma.
- *
- * @param string $term Term to modify
- * @param string $action Karma action (either ++ or --)
+ * Performs garbage collection on the antithrottling log.
*
* @return void
*/
- protected function modifyKarma($term, $action)
+ public function doGc()
{
- if (empty($term)) {
- return;
- }
-
- $karma = $this->fetchKarma($term);
- if ($karma !== false) {
- $statement = $this->updateKarma;
- } else {
- $statement = $this->insertKarma;
- }
-
- $karma += ($action == '++') ? 1 : -1;
-
- $args = array(
- ':term' => $term,
- ':karma' => $karma
- );
- $statement->execute($args);
- }
-
- /**
- * Returns the karma rating for a specified term for which the karma
- * rating can be modified.
- *
- * @param string $term Term for which to fetch the corresponding karma
- * rating
- *
- * @return integer|boolean Integer value denoting the term's karma or
- * FALSE if there is the specified term has no associated karma
- * rating
- */
- protected function fetchKarma($term)
- {
- $this->fetchKarma->execute(array(':term' => $term));
- $result = $this->fetchKarma->fetch(PDO::FETCH_ASSOC);
-
- if ($result === false) {
- return false;
- }
-
- return (int) $result['karma'];
- }
-
- /**
- * Returns a phrase describing the karma rating for a specified term for
- * which the karma rating is fixed.
- *
- * @param string $term Term for which to fetch the corresponding karma
- * rating
- *
- * @return string Phrase describing the karma rating, which may be append
- * to the term to form a complete response
- */
- protected function fetchFixedKarma($term)
- {
- $this->fetchFixedKarma->execute(array(':term' => $term));
- $result = $this->fetchFixedKarma->fetch(PDO::FETCH_ASSOC);
-
- if ($result === false) {
- return false;
- }
-
- return $result['karma'];
+ unset($this->log);
+ $this->log = array();
+ $this->lastGc = date('d');
}
}