2003-03-12 07:26:59 +01:00
|
|
|
#!/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.
|
|
|
|
###
|
|
|
|
|
2003-03-25 07:53:51 +01:00
|
|
|
"""
|
2003-11-15 05:37:04 +01:00
|
|
|
Silently listens to every message received on a channel and keeps statistics
|
|
|
|
concerning joins, parts, and various other commands in addition to tracking
|
|
|
|
statistics about smileys, actions, characters, and words.
|
|
|
|
"""
|
2003-03-25 07:53:51 +01:00
|
|
|
|
2003-11-25 09:23:47 +01:00
|
|
|
__revision__ = "$Id$"
|
|
|
|
|
2003-10-05 14:56:56 +02:00
|
|
|
import plugins
|
2003-03-12 07:26:59 +01:00
|
|
|
|
2003-10-04 15:53:13 +02:00
|
|
|
import os
|
2003-03-12 07:26:59 +01:00
|
|
|
import re
|
2003-08-12 21:38:23 +02:00
|
|
|
import sets
|
2003-03-12 07:26:59 +01:00
|
|
|
import time
|
2003-10-02 02:13:44 +02:00
|
|
|
import getopt
|
2003-11-05 08:59:44 +01:00
|
|
|
import string
|
2003-11-15 05:37:04 +01:00
|
|
|
from itertools import imap, ifilter
|
2003-03-12 07:26:59 +01:00
|
|
|
|
2004-02-03 06:29:40 +01:00
|
|
|
import log
|
2003-10-06 23:17:21 +02:00
|
|
|
import conf
|
2003-04-12 12:39:04 +02:00
|
|
|
import utils
|
2004-02-03 06:29:40 +01:00
|
|
|
import world
|
2003-04-01 10:38:53 +02:00
|
|
|
import ircdb
|
2004-02-03 06:29:40 +01:00
|
|
|
import irclib
|
2003-04-01 10:38:53 +02:00
|
|
|
import ircmsgs
|
2003-11-02 19:59:06 +01:00
|
|
|
import plugins
|
2003-03-12 07:26:59 +01:00
|
|
|
import ircutils
|
2004-01-28 20:08:47 +01:00
|
|
|
import privmsgs
|
|
|
|
import registry
|
2003-03-12 07:26:59 +01:00
|
|
|
import callbacks
|
|
|
|
|
2004-01-08 01:48:30 +01:00
|
|
|
# I should write/copy a generalized proxy at some point.
|
2004-01-28 20:08:47 +01:00
|
|
|
class Smileys(registry.Value):
|
|
|
|
def set(self, s):
|
|
|
|
L = s.split()
|
|
|
|
self.setValue(L)
|
2004-01-08 01:48:30 +01:00
|
|
|
|
2004-01-28 20:08:47 +01:00
|
|
|
def setValue(self, v):
|
2004-01-30 07:11:51 +01:00
|
|
|
self.s = ' '.join(v)
|
2004-01-28 20:08:47 +01:00
|
|
|
self.value = re.compile('|'.join(imap(re.escape, v)))
|
2004-01-08 01:48:30 +01:00
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return self.s
|
|
|
|
|
2004-01-30 07:11:51 +01:00
|
|
|
conf.registerPlugin('ChannelStats')
|
|
|
|
conf.registerChannelValue(conf.supybot.plugins.ChannelStats, 'selfStats',
|
2004-01-28 20:08:47 +01:00
|
|
|
registry.Boolean(True, """Determines whether the bot will keep channel
|
|
|
|
statistics on itself, possibly skewing the channel stats (especially in
|
|
|
|
cases where the bot is relaying between channels on a network)."""))
|
2004-01-30 07:11:51 +01:00
|
|
|
conf.registerChannelValue(conf.supybot.plugins.ChannelStats, 'smileys',
|
2004-01-28 20:08:47 +01:00
|
|
|
Smileys(':) ;) ;] :-) :-D :D :P :p (= =)'.split(), """Determines what
|
|
|
|
words (i.e., pieces of text with no spaces in them) are considered
|
|
|
|
'smileys' for the purposes of stats-keeping."""))
|
2004-01-30 07:11:51 +01:00
|
|
|
conf.registerChannelValue(conf.supybot.plugins.ChannelStats, 'frowns',
|
2004-01-28 20:08:47 +01:00
|
|
|
Smileys(':| :-/ :-\\ :\\ :/ :( :-( :\'('.split(), """Determines what words
|
|
|
|
(i.e., pieces of text with no spaces in them ) are considered 'frowns' for
|
|
|
|
the purposes of stats-keeping."""))
|
|
|
|
|
2004-02-03 06:29:40 +01:00
|
|
|
class ChannelStat(irclib.IrcCommandDispatcher):
|
|
|
|
def __init__(self, actions=0, chars=0, frowns=0, joins=0, kicks=0, modes=0,
|
|
|
|
msgs=0, parts=0, quits=0, smileys=0, topics=0, words=0):
|
|
|
|
self.actions = actions
|
|
|
|
self.chars = chars
|
|
|
|
self.frowns = frowns
|
|
|
|
self.joins = joins
|
|
|
|
self.kicks = kicks
|
|
|
|
self.modes = modes
|
|
|
|
self.msgs = msgs
|
|
|
|
self.parts = parts
|
|
|
|
self.quits = quits
|
|
|
|
self.smileys = smileys
|
|
|
|
self.topics = topics
|
|
|
|
self.words = words
|
|
|
|
self._values = ['actions', 'chars', 'frowns', 'joins', 'kicks','modes',
|
|
|
|
'msgs', 'parts', 'quits', 'smileys', 'topics', 'words']
|
|
|
|
def values(self):
|
|
|
|
return map(curry(getattr, self), self._values)
|
|
|
|
|
|
|
|
def addMsg(self, msg):
|
|
|
|
self.msgs += 1
|
|
|
|
method = self.dispatchCommand(msg.command)
|
|
|
|
if method is not None:
|
|
|
|
method(msg)
|
|
|
|
|
|
|
|
def doPayload(self, channel, payload):
|
|
|
|
self.chars += len(payload)
|
|
|
|
self.words += len(payload.split())
|
|
|
|
fRe = conf.supybot.plugins.ChannelStats.get('frowns').get(channel)()
|
|
|
|
sRe =conf.supybot.plugins.ChannelStats.get('smileys').get(channel)()
|
|
|
|
self.frowns += len(fRe.findall(payload))
|
|
|
|
self.smileys += len(sRe.findall(payload))
|
|
|
|
|
|
|
|
def doPrivmsg(self, msg):
|
|
|
|
self.doPayload(*msg.args)
|
|
|
|
if ircmsgs.isAction(msg):
|
|
|
|
self.actions += 1
|
|
|
|
|
|
|
|
def doTopic(self, msg):
|
|
|
|
self.doPayload(*msg.args)
|
|
|
|
self.topics += 1
|
|
|
|
|
|
|
|
def doKick(self, msg):
|
|
|
|
self.kicks += 1
|
|
|
|
|
|
|
|
def doPart(self, msg):
|
2004-02-03 23:59:09 +01:00
|
|
|
if len(msg.args) == 2:
|
|
|
|
self.doPayload(*msg.args)
|
2004-02-03 06:29:40 +01:00
|
|
|
self.parts += 1
|
|
|
|
|
|
|
|
def doMode(self, msg):
|
|
|
|
self.modes += 1
|
|
|
|
|
|
|
|
|
|
|
|
class UserStat(ChannelStat):
|
|
|
|
def __init__(self, id, kicked=0, *args):
|
|
|
|
ChannelStat.__init__(self, *args)
|
|
|
|
self.id = id
|
|
|
|
self.kicked = kicked
|
|
|
|
self._values.insert(0, 'kicked')
|
|
|
|
|
|
|
|
def doKick(self, msg):
|
|
|
|
self.doPayload(msg.args[0], msg.args[2])
|
|
|
|
self.kicks += 1
|
|
|
|
|
|
|
|
class StatsDB(object):
|
|
|
|
def __init__(self, filename):
|
|
|
|
self.filename = filename
|
|
|
|
self.channels = ircutils.IrcDict()
|
|
|
|
self.channelStats = ircutils.IrcDict()
|
|
|
|
try:
|
|
|
|
fd = file(filename)
|
|
|
|
except EnvironmentError, e:
|
|
|
|
log.info('Couldn\'t open %s: %s', filename, e)
|
|
|
|
return
|
|
|
|
lineno = 0
|
|
|
|
for line in fd:
|
|
|
|
lineno += 1
|
|
|
|
line = line.rstrip('\r\n')
|
|
|
|
try:
|
|
|
|
(channelOrId, rest) = line.split(':', 1)
|
|
|
|
if ircutils.isChannel(channelOrId):
|
|
|
|
channel = channelOrId
|
|
|
|
values = map(int, rest.split(','))
|
|
|
|
self.channelStats[channel] = ChannelStat(*values)
|
|
|
|
else:
|
|
|
|
id = int(channelOrId)
|
|
|
|
(channel, rest) = rest.split(':', 1)
|
|
|
|
d = self.channels.setdefault(channel, {})
|
|
|
|
values = map(int, rest.split(','))
|
|
|
|
d[id] = UserStat(id, *values)
|
|
|
|
except ValueError:
|
|
|
|
log.warning('Invalid line (#%s): %r', lineno, line)
|
|
|
|
continue
|
|
|
|
fd.close()
|
|
|
|
|
|
|
|
def flush(self):
|
|
|
|
fd = file(self.filename, 'w')
|
|
|
|
L = self.channelStats.items()
|
|
|
|
L.sort()
|
|
|
|
for (channel, stat) in L:
|
|
|
|
fd.write('%s:%s' % (channel, ','.join(map(str, stat.values()))))
|
|
|
|
fd.write(os.linesep)
|
|
|
|
if channel not in self.channels:
|
|
|
|
continue
|
|
|
|
d = self.channels[channel]
|
|
|
|
LL = d.items()
|
|
|
|
LL.sort()
|
|
|
|
for (id, stat) in LL:
|
|
|
|
fd.write('%s:%s:%s' % (id, channel,
|
|
|
|
','.join(map(str, stat.values()))))
|
|
|
|
fd.write(os.linesep)
|
|
|
|
fd.close()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
self.flush()
|
|
|
|
self.channels.clear()
|
|
|
|
self.channelStats.clear()
|
|
|
|
|
|
|
|
def addMsg(self, msg, id=None):
|
|
|
|
channel = msg.args[0]
|
|
|
|
if ircutils.isChannel(channel):
|
|
|
|
if channel not in self.channelStats:
|
|
|
|
self.channelStats[channel] = ChannelStat()
|
|
|
|
self.channelStats[channel].addMsg(msg)
|
|
|
|
try:
|
|
|
|
if id is None:
|
|
|
|
id = ircdb.users.getUserId(msg.prefix)
|
|
|
|
except KeyError:
|
|
|
|
return
|
|
|
|
if channel not in self.channels:
|
|
|
|
self.channels[channel] = {}
|
|
|
|
if id not in self.channels[channel]:
|
|
|
|
self.channels[channel][id] = UserStat(id)
|
|
|
|
self.channels[channel][id].addMsg(msg)
|
|
|
|
|
|
|
|
def get(self, channel, id=None):
|
|
|
|
if id is None:
|
|
|
|
return self.channelStats[channel]
|
|
|
|
else:
|
|
|
|
return self.channels[channel][id]
|
|
|
|
|
2004-01-28 20:08:47 +01:00
|
|
|
|
2004-02-03 06:29:40 +01:00
|
|
|
class ChannelStats(callbacks.Privmsg):
|
2003-11-11 00:39:44 +01:00
|
|
|
noIgnore = True
|
2003-03-12 07:26:59 +01:00
|
|
|
def __init__(self):
|
2003-11-04 09:42:11 +01:00
|
|
|
callbacks.Privmsg.__init__(self)
|
2003-10-16 15:34:02 +02:00
|
|
|
self.lastmsg = None
|
|
|
|
self.laststate = None
|
2003-10-25 00:29:30 +02:00
|
|
|
self.outFiltering = False
|
2004-02-03 06:29:40 +01:00
|
|
|
self.db = StatsDB(os.path.join(conf.supybot.directories.data(),
|
|
|
|
'ChannelStats.db'))
|
|
|
|
world.flushers.append(self.db.flush)
|
2003-03-12 07:26:59 +01:00
|
|
|
|
2003-11-11 16:58:20 +01:00
|
|
|
def die(self):
|
2004-02-03 06:29:40 +01:00
|
|
|
world.flushers.remove(self.db.flush)
|
|
|
|
self.db.close()
|
2003-11-11 16:58:20 +01:00
|
|
|
callbacks.Privmsg.die(self)
|
2003-08-20 18:26:23 +02:00
|
|
|
|
2003-10-16 15:34:02 +02:00
|
|
|
def __call__(self, irc, msg):
|
|
|
|
try:
|
|
|
|
if self.lastmsg:
|
|
|
|
self.laststate.addMsg(irc, self.lastmsg)
|
|
|
|
else:
|
|
|
|
self.laststate = irc.state.copy()
|
|
|
|
finally:
|
|
|
|
self.lastmsg = msg
|
2004-02-03 06:29:40 +01:00
|
|
|
self.db.addMsg(msg)
|
2004-01-30 07:11:51 +01:00
|
|
|
super(ChannelStats, self).__call__(irc, msg)
|
2003-10-16 15:34:02 +02:00
|
|
|
|
2003-10-25 00:29:30 +02:00
|
|
|
def outFilter(self, irc, msg):
|
|
|
|
if msg.command == 'PRIVMSG':
|
|
|
|
if ircutils.isChannel(msg.args[0]):
|
2004-01-28 20:08:47 +01:00
|
|
|
if self.registryValue('selfStats', msg.args[0]):
|
2003-11-02 19:59:06 +01:00
|
|
|
try:
|
|
|
|
self.outFiltering = True
|
2004-02-03 06:29:40 +01:00
|
|
|
self.db.addMsg(msg, 0)
|
2003-11-02 19:59:06 +01:00
|
|
|
finally:
|
|
|
|
self.outFiltering = False
|
2003-10-25 00:29:30 +02:00
|
|
|
return msg
|
2003-03-12 07:26:59 +01:00
|
|
|
|
2004-02-03 06:29:40 +01:00
|
|
|
def doQuit(self, irc, msg):
|
2003-04-16 08:44:30 +02:00
|
|
|
try:
|
2003-10-20 09:39:40 +02:00
|
|
|
id = ircdb.users.getUserId(msg.prefix)
|
2003-04-16 08:44:30 +02:00
|
|
|
except KeyError:
|
2004-02-03 06:29:40 +01:00
|
|
|
id = None
|
|
|
|
for (channel, c) in self.laststate.channels.iteritems():
|
|
|
|
if msg.nick in c.users:
|
|
|
|
if channel not in self.db.channelStats:
|
|
|
|
self.db.channelStats[channel] = ChannelStat()
|
|
|
|
self.db.channelStats[channel].quits += 1
|
|
|
|
if id is not None:
|
|
|
|
if channel not in self.db.channels:
|
|
|
|
self.db.channels[channel] = {}
|
|
|
|
if id not in self.db.channels[channel]:
|
|
|
|
self.db.channels[channel][id] = UserStat(id)
|
|
|
|
self.db.channels[channel][id].quits += 1
|
2003-03-26 11:04:26 +01:00
|
|
|
|
|
|
|
def doKick(self, irc, msg):
|
2004-02-03 06:29:40 +01:00
|
|
|
(channel, nick, _) = msg.args
|
|
|
|
hostmask = irc.state.nickToHostmask(nick)
|
2003-03-26 11:04:26 +01:00
|
|
|
try:
|
2004-02-03 06:29:40 +01:00
|
|
|
id = ircdb.users.getUserId(hostmask)
|
2003-03-26 11:04:26 +01:00
|
|
|
except KeyError:
|
2004-02-03 06:29:40 +01:00
|
|
|
return
|
|
|
|
if channel not in self.db.channels:
|
|
|
|
self.db.channels[channel] = {}
|
|
|
|
if id not in self.db.channels[channel]:
|
|
|
|
self.db.channels[channel][id] = UserStat(id)
|
|
|
|
self.db.channels[channel][id].kicked += 1
|
2003-10-16 15:34:02 +02:00
|
|
|
|
2003-03-26 11:04:26 +01:00
|
|
|
def stats(self, irc, msg, args):
|
2003-10-23 22:59:51 +02:00
|
|
|
"""[<channel>] [<name>]
|
2003-04-16 08:44:30 +02:00
|
|
|
|
2003-10-23 22:59:51 +02:00
|
|
|
Returns the statistics for <name> on <channel>. <channel> is only
|
|
|
|
necessary if the message isn't sent on the channel itself. If <name>
|
|
|
|
isn't given, it defaults to the user sending the command.
|
2003-04-16 08:44:30 +02:00
|
|
|
"""
|
2003-03-26 11:04:26 +01:00
|
|
|
channel = privmsgs.getChannel(msg, args)
|
2003-11-11 14:20:06 +01:00
|
|
|
name = privmsgs.getArgs(args, required=0, optional=1)
|
2004-01-31 22:03:31 +01:00
|
|
|
if name == irc.nick:
|
|
|
|
id = 0
|
|
|
|
elif not name:
|
2003-10-23 22:59:51 +02:00
|
|
|
try:
|
|
|
|
id = ircdb.users.getUserId(msg.prefix)
|
2003-10-25 00:29:30 +02:00
|
|
|
name = ircdb.users.getUser(id).name
|
2003-10-23 22:59:51 +02:00
|
|
|
except KeyError:
|
2004-01-08 04:12:14 +01:00
|
|
|
irc.error('I couldn\'t find you in my user database.')
|
2003-10-23 22:59:51 +02:00
|
|
|
return
|
|
|
|
elif not ircdb.users.hasUser(name):
|
2003-03-26 11:04:26 +01:00
|
|
|
try:
|
2003-10-02 02:43:38 +02:00
|
|
|
hostmask = irc.state.nickToHostmask(name)
|
2003-10-20 09:39:40 +02:00
|
|
|
id = ircdb.users.getUserId(hostmask)
|
2003-03-26 11:04:26 +01:00
|
|
|
except KeyError:
|
2004-01-08 16:47:38 +01:00
|
|
|
irc.errorNoUser()
|
2003-03-26 11:04:26 +01:00
|
|
|
return
|
2003-10-20 09:39:40 +02:00
|
|
|
else:
|
|
|
|
id = ircdb.users.getUserId(name)
|
2004-02-03 06:29:40 +01:00
|
|
|
try:
|
|
|
|
stats = self.db.get(channel, id)
|
|
|
|
s = '%s has sent %s; a total of %s, %s, ' \
|
|
|
|
'%s, and %s; %s of those messages %s' \
|
|
|
|
'%s has joined %s, parted %s, quit %s, kicked someone %s, ' \
|
|
|
|
'been kicked %s, changed the topic %s, ' \
|
|
|
|
'and changed the mode %s.' % \
|
|
|
|
(name, utils.nItems('message', stats.msgs),
|
|
|
|
utils.nItems('character', stats.chars),
|
|
|
|
utils.nItems('word', stats.words),
|
|
|
|
utils.nItems('smiley', stats.smileys),
|
|
|
|
utils.nItems('frown', stats.frowns),
|
|
|
|
stats.actions, stats.actions == 1 and 'was an ACTION. '
|
|
|
|
or 'were ACTIONs. ',
|
|
|
|
name,
|
|
|
|
utils.nItems('time', stats.joins),
|
|
|
|
utils.nItems('time', stats.parts),
|
|
|
|
utils.nItems('time', stats.quits),
|
|
|
|
utils.nItems('time', stats.kicks),
|
|
|
|
utils.nItems('time', stats.kicked),
|
|
|
|
utils.nItems('time', stats.topics),
|
|
|
|
utils.nItems('time', stats.modes))
|
|
|
|
irc.reply(s)
|
|
|
|
except KeyError:
|
|
|
|
irc.error('I have no stats for that %s in %s' % (name, channel))
|
2003-03-12 07:26:59 +01:00
|
|
|
|
2003-04-16 08:44:30 +02:00
|
|
|
def channelstats(self, irc, msg, args):
|
|
|
|
"""[<channel>]
|
|
|
|
|
|
|
|
Returns the statistics for <channel>. <channel> is only necessary if
|
|
|
|
the message isn't sent on the channel itself.
|
|
|
|
"""
|
|
|
|
channel = privmsgs.getChannel(msg, args)
|
2004-02-03 06:29:40 +01:00
|
|
|
try:
|
|
|
|
stats = self.db.get(channel)
|
|
|
|
s = 'On %s there have been %s messages, containing %s ' \
|
|
|
|
'characters, %s, %s, and %s; ' \
|
|
|
|
'%s of those messages %s. There have been ' \
|
|
|
|
'%s, %s, %s, %s, %s, and %s.' % \
|
|
|
|
(channel, stats.msgs, stats.chars,
|
|
|
|
utils.nItems('word', stats.words),
|
|
|
|
utils.nItems('smiley', stats.smileys),
|
|
|
|
utils.nItems('frown', stats.frowns),
|
|
|
|
stats.actions, stats.actions == 1 and 'was an ACTION'
|
|
|
|
or 'were ACTIONs',
|
|
|
|
utils.nItems('join', stats.joins),
|
|
|
|
utils.nItems('part', stats.parts),
|
|
|
|
utils.nItems('quit', stats.quits),
|
|
|
|
utils.nItems('kick', stats.kicks),
|
|
|
|
utils.nItems('change', stats.modes, between='mode'),
|
|
|
|
utils.nItems('change', stats.topics, between='topic'))
|
|
|
|
irc.reply(s)
|
|
|
|
except KeyError:
|
|
|
|
irc.error('I\'ve never been on %s.' % channel)
|
2003-04-16 08:44:30 +02:00
|
|
|
|
2003-11-06 07:41:04 +01:00
|
|
|
|
2004-01-30 07:11:51 +01:00
|
|
|
Class = ChannelStats
|
2003-03-26 11:04:26 +01:00
|
|
|
|
2003-03-24 09:41:19 +01:00
|
|
|
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|