From 214b7960bf13537bb551531219b227603e431c14 Mon Sep 17 00:00:00 2001 From: Jeremy Fincher Date: Tue, 27 Jan 2004 20:06:28 +0000 Subject: [PATCH] Initial checkin. These don't work, but I figured I'd add them anyway. --- plugins/WordStats.py | 272 +++++++++++++++++++++++++++++++++++++++++ test/test_WordStats.py | 165 +++++++++++++++++++++++++ 2 files changed, 437 insertions(+) create mode 100644 plugins/WordStats.py create mode 100644 test/test_WordStats.py diff --git a/plugins/WordStats.py b/plugins/WordStats.py new file mode 100644 index 000000000..49248d5f5 --- /dev/null +++ b/plugins/WordStats.py @@ -0,0 +1,272 @@ +#!/usr/bin/python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +""" +Keeps statistics on who says what words in a channel. +""" + +import plugins + +import conf +import utils +import plugins +import privmsgs +import callbacks + + +def configure(advanced): + from questions import expect, anything, something, yn + conf.registerPlugin('WordStats', True) + +conf.registerPlugin('WordStats') +conf.registerChannelValue(conf.supybot.plugins.WordStats, + 'wordstatsRankingDisplay', + registry.PositiveInteger(3, """Determines the maximum number of top users + to show for a given wordstat when someone requests the wordstats for a + particular word.""")) + +class WordStats(callbacks.Privmsg, plugins.ChannelDBHandler): + noIgnore = True + def die(self): + callbacks.Privmsg.die(self) + plugins.ChannelDBHandler.die(self) + + def makeDb(self, filename): + if os.path.exists(filename): + db = sqlite.connect(filename) + else: + db = sqlite.connect(filename) + cursor = db.cursor() + cursor.execute("""CREATE TABLE words ( + id INTEGER PRIMARY KEY, + word TEXT UNIQUE ON CONFLICT IGNORE + )""") + cursor.execute("""CREATE TABLE word_stats ( + id INTEGER PRIMARY KEY, + word_id INTEGER, + user_id INTEGER, + count INTEGER, + UNIQUE (word_id, user_id) ON CONFLICT IGNORE + )""") + cursor.execute("""CREATE INDEX word_stats_word_id + ON word_stats (word_id)""") + cursor.execute("""CREATE INDEX word_stats_user_id + ON word_stats (user_id)""") + db.commit() + return db + + _alphanumeric = string.ascii_letters + string.digits + _nonAlphanumeric = string.ascii.translate(string.ascii, _alphanumeric) + def doPrivmsg(self, irc, msg): + if not ircutils.isChannel(msg.args[0]): + return + callbacks.Privmsg.doPrivmsg(self, irc, msg) + try: + id = ircdb.users.getUserId(msg.prefix) + except KeyError: + return + (channel, s) = msg.args + s = s.strip() + if not s: + return + db = self.getDb(channel) + cursor = db.cursor() + words = s.lower().split() + words = [s.strip(self._nonAlphanumeric) for s in words] + criteria = ['word=%s'] * len(words) + criterion = ' OR '.join(criteria) + cursor.execute("SELECT id, word FROM words WHERE %s"%criterion, *words) + for (wordId, word) in cursor.fetchall(): + cursor.execute("""INSERT INTO word_stats + VALUES(NULL, %s, %s, 0)""", wordId, id) + cursor.execute("""UPDATE word_stats SET count=count+%s + WHERE word_id=%s AND user_id=%s""", + words.count(word), wordId, id) + db.commit() + + def add(self, irc, msg, args): + """[] + + Keeps stats on in . is only necessary if the + message isn't sent in the channel itself. + """ + channel = privmsgs.getChannel(msg, args) + word = privmsgs.getArgs(args) + word = word.strip() + if word.strip(self._nonAlphanumeric) != word: + irc.error(' must not contain non-alphanumeric chars.') + return + word = word.lower() + db = self.getDb(channel) + cursor = db.cursor() + cursor.execute("""INSERT INTO words VALUES (NULL, %s)""", word) + db.commit() + irc.replySuccess() + + def stats(self, irc, msg, args): + """[] [] [] + + With no arguments, returns the list of words that are being monitored + for stats. With alone, returns all the stats for that user. + With alone, returns the top users for that word. With + and , returns that user's stat for that word. is only + needed if not said in the channel. (Note: if only one of or + is given, is assumed first and only if no stats are + available for that word, do we assume it's .) + """ + channel = privmsgs.getChannel(msg, args) + (arg1, arg2) = privmsgs.getArgs(args, required=0, optional=2) + db = self.getDb(channel) + cursor = db.cursor() + if not arg1 and not arg2: + cursor.execute("""SELECT word FROM words""") + if cursor.rowcount == 0: + irc.reply('I am not currently keeping any word stats.') + return + l = [repr(tup[0]) for tup in cursor.fetchall()] + s = 'Currently keeping stats for: %s' % utils.commaAndify(l) + irc.reply(s) + elif arg1 and arg2: + user, word = (arg1, arg2) + try: + id = ircdb.users.getUserId(user) + except KeyError: # Maybe it was a nick. Check the hostmask. + try: + hostmask = irc.state.nickToHostmask(user) + id = ircdb.users.getUserId(hostmask) + except KeyError: + irc.errorNoUser() + return + db = self.getDb(channel) + cursor = db.cursor() + word = word.lower() + cursor.execute("""SELECT word_stats.count FROM words, word_stats + WHERE words.word=%s AND + word_id=words.id AND + word_stats.user_id=%s""", word, id) + if cursor.rowcount == 0: + cursor.execute("""SELECT id FROM words WHERE word=%s""", word) + if cursor.rowcount == 0: + irc.error('I\'m not keeping stats on %r.' % word) + else: + irc.error('%s has never said %r.' % (user, word)) + return + count = int(cursor.fetchone()[0]) + s = '%s has said %r %s.' % (user,word,utils.nItems('time', count)) + irc.reply(s) + else: + # Figure out if we got a user or a word + cursor.execute("""SELECT word FROM words + WHERE word=%s""", arg1) + if cursor.rowcount == 0: + # It was a user. + try: + id = ircdb.users.getUserId(arg1) + except KeyError: # Maybe it was a nick. Check the hostmask. + try: + hostmask = irc.state.nickToHostmask(arg1) + id = ircdb.users.getUserId(hostmask) + except KeyError: + irc.error('%r doesn\'t look like a user I know ' + 'or a word that I\'m keeping stats ' + 'on' % arg1) + return + cursor.execute("""SELECT words.word, word_stats.count + FROM words, word_stats + WHERE words.id = word_stats.word_id + AND word_stats.user_id=%s + ORDER BY words.word""", id) + if cursor.rowcount == 0: + username = ircdb.users.getUser(id).name + irc.error('%r has no wordstats' % username) + return + L = [('%r: %s' % (word, count)) for + (word, count) in cursor.fetchall()] + irc.reply(utils.commaAndify(L)) + return + else: + # It's a word, not a user + word = arg1 + numUsers = self.registryValue('wordstatsRankingDisplay', + msg.args[0]) + cursor.execute("""SELECT word_stats.count, + word_stats.user_id + FROM words, word_stats + WHERE words.word=%s AND + words.id=word_stats.word_id + ORDER BY word_stats.count DESC""", + word) + if cursor.rowcount == 0: + irc.error('No one has said %r' % word) + return + results = cursor.fetchall() + numResultsShown = min(cursor.rowcount, numUsers) + cursor.execute("""SELECT sum(word_stats.count) + FROM words, word_stats + WHERE words.word=%s AND + words.id=word_stats.word_id""", + word) + total = int(cursor.fetchone()[0]) + ers = '%rer' % word + ret = 'Top %s ' % utils.nItems(ers, numResultsShown) + ret += '(out of a total of %s seen):' % \ + utils.nItems(repr(word), total) + L = [] + for (count, id) in results[:numResultsShown]: + username = ircdb.users.getUser(id).name + L.append('%s: %s' % (username, count)) + try: + id = ircdb.users.getUserId(msg.prefix) + rank = 1 + s = "" # Don't say anything if they show in the output + # already + seenUser = False + for (count, userId) in results: + if userId == id: + seenUser = True + if rank > numResultsShown: + s = 'You are ranked %s out of %s with %s.' % \ + (rank, utils.nItems(ers, len(results)), + utils.nItems(repr(word), count)) + break + else: + rank += 1 + else: + if not seenUser: + s = 'You have not said %r' % word + ret = '%s %s. %s' % (ret, utils.commaAndify(L), s) + except KeyError: + ret = '%s %s.' % (ret, utils.commaAndify(L)) + irc.reply(ret) + +Class = WordStats + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: diff --git a/test/test_WordStats.py b/test/test_WordStats.py new file mode 100644 index 000000000..0a47d061b --- /dev/null +++ b/test/test_WordStats.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python + +### +# Copyright (c) 2002, Jeremiah Fincher +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +from testsupport import * + +import ircdb + +try: + import sqlite +except ImportError: + sqlite = None + +if sqlite is not None: + class ChannelDBTestCase(ChannelPluginTestCase): + plugins = ('WordStats', 'User') + def setUp(self): + ChannelPluginTestCase.setUp(self) + self.prefix = 'foo!bar@baz' + self.nick = 'foo' + self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, + 'register foo bar', + prefix=self.prefix)) + _ = self.irc.takeMsg() + ircdb.users.getUser(self.nick).addCapability(self.channel + '.op') + + def testWordStatsNoArgs(self): + self.assertResponse('wordstats', 'I am not currently keeping any ' + 'word stats.') + self.assertNotError('addword lol') + self.assertResponse('wordstats', 'Currently keeping stats for: ' + '\'lol\'') + + def testWordStatsUser(self): + self.assertNotError('addword lol') + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol', + prefix=self.prefix)) + self.assertResponse('wordstats foo', '\'lol\': 2') + self.assertNotError('addword moo') + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'moo', + prefix=self.prefix)) + self.assertResponse('wordstats foo', '\'lol\': 2 and \'moo\': 2') + + def testWordStatsWord(self): + userPrefix1 = 'moo!bar@baz'; userNick1 = 'moo' + userPrefix2 = 'boo!bar@baz'; userNick2 = 'boo' + self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, + 'register %s bar' % userNick1, + prefix=userPrefix1)) + self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, + 'register %s bar' % userNick2, + prefix=userPrefix2)) + _ = self.irc.takeMsg() + _ = self.irc.takeMsg() + self.assertNotError('addword lol') + self.assertRegexp('wordstats lol', 'foo: 1') + for i in range(5): + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol', + prefix=userPrefix1)) + self.assertRegexp('wordstats lol', + '2.*%s: 5.*foo: 2' % userNick1) + for i in range(10): + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol', + prefix=userPrefix2)) + self.assertRegexp('wordstats lol', + '3.*%s: 10.*%s: 5.*foo: 3' % + (userNick2, userNick1)) + # Check for the extra-swanky stuff too + # (note: to do so we must make sure they don't appear in the list, + # so we'll tweak the config) + self.assertNotError('channeldb config wordstats-top-n 2') + self.assertRegexp('wordstats lol', + 'total.*19 \'lol\'s.*%s: 10.*%s: 5.*' + 'ranked 3 out of 3 \'lol\'ers' % \ + (userNick2, userNick1)) + + def testWordStatsUserWord(self): + self.assertNotError('addword lol') + self.assertResponse('wordstats foo lol', + 'foo has said \'lol\' 1 time.') + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol', + prefix=self.prefix)) + self.assertResponse('wordstats foo lol', + 'foo has said \'lol\' 3 times.') + # Now check for case-insensitivity + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'LOL', + prefix=self.prefix)) + self.assertResponse('wordstats foo lol', + 'foo has said \'lol\' 5 times.') + # Check and make sure actions get nabbed too + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol', + prefix=self.prefix)) + self.assertResponse('wordstats foo lol', + 'foo has said \'lol\' 7 times.') + # Check and make sure it handles two words in one message + self.assertNotError('addword heh') + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol heh', + prefix=self.prefix)) + self.assertResponse('wordstats foo lol', + 'foo has said \'lol\' 9 times.') + self.assertResponse('wordstats foo heh', + 'foo has said \'heh\' 2 times.') + # It should ignore punctuation around words + self.irc.feedMsg(ircmsgs.privmsg(self.channel,'lol, I said "heh"', + prefix=self.prefix)) + self.assertResponse('wordstats foo lol', + 'foo has said \'lol\' 11 times.') + self.assertResponse('wordstats foo heh', + 'foo has said \'heh\' 4 times.') + + def testAddword(self): + self.assertError('addword lol!') + self.assertNotError('addword lolz0r') + + def testWordStatsTopN(self): + self.assertNotError('addword lol') + self.assertNotError('channeldb config wordstats-top-n 5') + # Create 10 users and have them each send a different number of + # 'lol's to the channel + users = [] + for i in range(10): + users.append(('foo%s!bar@baz' % i, 'foo%s' % i)) + self.irc.feedMsg(ircmsgs.privmsg(self.irc.nick, + 'register %s bar' % \ + users[i][1], + prefix=users[i][0])) + _ = self.irc.takeMsg() + for i in range(10): + for j in range(i): + self.irc.feedMsg(ircmsgs.privmsg(self.channel, 'lol', + prefix=users[i][0])) + # Make sure it shows the top 5 + self.assertRegexp('wordstats lol', + 'Top 5 \'lol\'ers.*foo9: 9.*foo8: 8.*' + 'foo7: 7.*foo6: 6.*foo5: 5') + +# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: +