Supybot-plugins/WebStats/plugin.py

946 lines
36 KiB
Python
Raw Permalink Normal View History

2011-03-11 08:59:33 -08:00
# -*- coding: utf8 -*-
2010-11-19 11:06:38 -08:00
###
2011-03-11 08:59:33 -08:00
# Copyright (c) 2010-2011, Valentin Lorentz
2010-11-19 11:06:38 -08:00
# 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.
###
2011-03-11 08:59:33 -08:00
import re
2010-11-20 08:51:23 -08:00
import os
2010-11-19 11:06:38 -08:00
import sys
import time
import urllib
import random
import datetime
if sys.version_info[0] >= 3:
from io import BytesIO
else:
from cStringIO import StringIO as StringIO
2010-11-20 08:51:23 -08:00
import supybot.conf as conf
2010-11-19 11:06:38 -08:00
import supybot.world as world
import supybot.log as log
import supybot.conf as conf
2010-11-19 11:06:38 -08:00
import supybot.utils as utils
2012-09-03 01:58:04 -07:00
import supybot.ircdb as ircdb
2010-11-19 11:06:38 -08:00
from supybot.commands import *
2010-11-22 08:28:55 -08:00
import supybot.irclib as irclib
2010-11-19 11:06:38 -08:00
import supybot.plugins as plugins
2010-11-22 08:28:55 -08:00
import supybot.ircmsgs as ircmsgs
2010-11-19 11:06:38 -08:00
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
import supybot.httpserver as httpserver
2010-11-19 11:06:38 -08:00
2010-11-20 08:51:23 -08:00
try:
import sqlite3
except ImportError:
from pysqlite2 import dbapi2 as sqlite3 # for python2.4
try:
from supybot.i18n import _PluginInternationalization
class WebStatsInternationalization(_PluginInternationalization):
def __init__(self):
self.name = 'WebStats'
try:
self.loadLocale(conf.supybot.language())
except:
pass
_ = WebStatsInternationalization()
except ImportError:
2010-11-20 08:51:23 -08:00
_ = lambda x:x
internationalizeDocstring = lambda x:x
2010-11-19 11:06:38 -08:00
DEBUG = False
2010-11-20 08:51:23 -08:00
testing = world.testing
world.webStatsCacheLinks = {}
2010-11-19 11:06:38 -08:00
#####################################################################
# Utilities
#####################################################################
2010-11-19 11:06:38 -08:00
2010-11-20 08:51:23 -08:00
class FooException(Exception):
pass
2010-11-19 11:06:38 -08:00
if not hasattr(world, 'webStatsCacheLinks'):
world.webStatsCacheLinks = {}
colors = ['green', 'red', 'orange', 'blue', 'black', 'gray50', 'indigo']
def chooseColor(nick):
global colors
return random.choice(colors)
def progressbar(item, max_):
template = """<td class="progressbar">
<div class="text">%i</div>
<div style="width: %ipx; background-color: %s"
class="color"></div>
</td>"""
try:
percent = round(float(item)/float(max_)*100)
color = round((100-percent)/10)*3+59
template %= (item, percent, '#ef%i%i' % (color, color))
except ZeroDivisionError:
template %= (item, 0, 'orange')
return template
def fillTable(items, page, orderby=None):
output = ''
nbDisplayed = 0
max_ = [0, 0, 0, 0, 0, 0, 0, 0, 0]
for index in items:
for index_ in range(0, len(max_)):
max_[index_] = max(max_[index_], items[index][index_])
rowsList = []
while len(items) > 0:
maximumIndex = (0, 0, 0, 0, 0)
highScore = -1
for index in items:
if orderby is not None and items[index][orderby] > highScore:
maximumIndex = index
highScore = items[index][orderby]
if orderby is None and index < maximumIndex:
maximumIndex = index
item = items.pop(maximumIndex)
try:
int(index)
indexIsInt = True
except:
indexIsInt = False
if sum(item[0:1] + item[3:]) > 5 or indexIsInt:
rowsList.append((maximumIndex, item))
nbDisplayed += 1
for row in rowsList[int(page):int(page)+25]:
index, row = row
output += '<tr><td>%s</td>' % index
for cell in (progressbar(row[0], max_[0]),
progressbar(row[1], max_[1]),
progressbar(row[3], max_[3]),
progressbar(row[4], max_[4]),
progressbar(row[5], max_[5]),
progressbar(row[6], max_[6]),
progressbar(row[7], max_[7]),
progressbar(row[8], max_[8])
):
output += cell
output += '</tr>'
return output, nbDisplayed
headers = (_('Lines'), _('Words'), _('Joins'), _('Parts'),
_('Quits'), _('Nick changes'), _('Kicks'), _('Kicked'))
tableHeaders = '<table><tr><th><a href="%s">%s</a></th>'
for header in headers:
tableHeaders += '<th style="width: 150px;"><a href="%%s%s/">%s</a></th>' %\
(header, header)
tableHeaders += '</tr>'
nameToColumnIndex = {_('lines'):0,_('words'):1,_('chars'):2,_('joins'):3,
_('parts'):4,_('quits'):5,_('nick changes'):6,_('kickers'):7,
_('kicked'):8,_('kicks'):7}
def getTable(firstColumn, items, channel, urlLevel, page, orderby):
percentParameter = tuple()
for foo in range(1, len(tableHeaders.split('%s'))-1):
percentParameter += ('./' + '../'*(urlLevel-4),)
if len(percentParameter) == 1:
percentParameter += (firstColumn,)
output = tableHeaders % percentParameter
if orderby is not None:
if sys.version_info[0] >= 0:
orderby = urllib.parse.unquote(orderby)
else:
orderby = urllib.unquote(orderby)
try:
index = nameToColumnIndex[orderby]
html, nbDisplayed = fillTable(items, page, index)
except KeyError:
orderby = None
if orderby is None:
html, nbDisplayed = fillTable(items, page)
output += html
output += '</table>'
return output, nbDisplayed
#####################################################################
# Templates
#####################################################################
PAGE_SKELETON = """\
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Supybot WebStats</title>
2013-06-01 02:44:48 -07:00
<link rel="stylesheet" media="screen" type="text/css" title="Design" href="/default.css" />
<link rel="stylesheet" media="screen" type="text/css" title="Design" href="/webstats/design.css" />
</head>
2013-06-01 02:44:48 -07:00
<body %%s>
%%s
<p id="footer">
<a href="https://github.com/ProgVal/Limnoria">Limnoria</a> and
<a href="https://github.com/ProgVal/Supybot-plugins/tree/master/WebStats/">WebStats</a> powered.<br />
Libre software available under BSD licence.<br />
Page generated at %s.
</p>
</body>
</html>
""" % time.strftime('%H:%M:%S')
DEFAULT_TEMPLATES = {
'webstats/design.css': """\
body, html {
text-align: center;
}
li {
list-style-type: none;
}
#footer {
width: 100%;
font-size: 0.6em;
text-align: right;
}
.chanslist li a:visited {
color: blue;
}
table {
margin-left: auto;
margin-right: auto;
}
.progressbar {
border: orange 1px solid;
height: 20px;
}
.progressbar .color {
background-color: orange;
height: 20px;
text-align: center;
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
}
.progressbar .text {
position: absolute;
width: 150px;
text-align: center;
margin-top: auto;
margin-bottom: auto;
}""",
2013-06-01 02:44:48 -07:00
'webstats/index.html': PAGE_SKELETON % ('class="purelisting"', """\
<h1>%(title)s</h1>
<ul class="chanslist">
%(channels)s
2013-06-01 02:44:48 -07:00
</ul>"""),
'webstats/global.html': PAGE_SKELETON % ('', """\
<h1>Stats about %(channel)s channel</h1>
<p><a href="/webstats/nicks/%(escaped_channel)s/">View nick-by-nick stats</a></p>
<p><a href="/webstats/links/%(escaped_channel)s/">View links</a></p>
<p>There were %(quick_stats)s</p>
2013-06-01 02:44:48 -07:00
%(table)s"""),
'webstats/nicks.html': PAGE_SKELETON % ('', """\
<h1>Stats about %(channel)s channel</h1>
<p><a href="/webstats/global/%(escaped_channel)s/">View global stats</a></p>
<p><a href="/webstats/links/%(escaped_channel)s/">View links</a></p>
%(table)s
<p>%(pagination)s</p>
2013-06-01 02:44:48 -07:00
"""),
}
httpserver.set_default_templates(DEFAULT_TEMPLATES)
#####################################################################
# Controller
#####################################################################
class WebStatsServerCallback(httpserver.SupyHTTPServerCallback):
name = 'WebStats'
def doGet(self, handler, path):
2010-11-20 08:51:23 -08:00
output = ''
splittedPath = path.split('/')
2010-11-20 08:51:23 -08:00
try:
if path == '/design.css':
2010-11-20 08:51:23 -08:00
response = 200
content_type = 'text/css; charset=utf-8'
output = httpserver.get_template('webstats/design.css')
elif path == '/':
2010-11-20 08:51:23 -08:00
response = 200
content_type = 'text/html; charset=utf-8'
output = self.get_index()
elif path == '/global/':
response = 404
content_type = 'text/html; charset=utf-8'
output = """<p style="font-size: 20em">BAM!</p>
2012-09-03 01:58:04 -07:00
<p>You played with the URL, you lost.</p>"""
2011-03-11 08:59:33 -08:00
elif splittedPath[1] in ('nicks', 'global', 'links') \
and path[-1]=='/'\
or splittedPath[1] == 'nicks' and \
path.endswith('.htm'):
2010-11-20 08:51:23 -08:00
response = 200
content_type = 'text/html; charset=utf-8'
2011-03-11 08:59:33 -08:00
if splittedPath[1] == 'links':
try:
import pygraphviz
content_type = 'image/png'
except ImportError:
content_type = 'text/plain; charset=utf-8'
response = 501
self.send_response(response)
self.send_header('Content-type', content_type)
self.end_headers()
self.wfile.write('Links cannot be displayed; ask '
'the bot owner to install python-pygraphviz.')
self.wfile.close()
return
assert len(splittedPath) > 2
chanName = splittedPath[2].replace('%20', '#')
page = splittedPath[-1][0:-len('.htm')]
if page == '':
page = '0'
if splittedPath[1] == 'nicks':
formatter = self.get_nicks
elif splittedPath[1] == 'global':
formatter = self.get_global
elif splittedPath[1] == 'links':
formatter = self.get_links
else:
raise AssertionError(splittedPath[1])
if len(splittedPath) == 3:
_.loadLocale(self.plugin._getLanguage(chanName))
output = formatter(len(splittedPath), chanName, page)
else:
assert len(splittedPath) > 3
_.loadLocale(self.plugin._getLanguage(chanName))
subdir = splittedPath[3].lower()
output = formatter(len(splittedPath), chanName, page,
subdir)
2010-11-20 08:51:23 -08:00
else:
response = 404
content_type = 'text/html; charset=utf-8'
output = httpserver.get_template('generic/error.html') % \
{'title': 'WebStats - not found',
'error': 'Requested page is not found. Sorry.'}
except Exception as e:
2010-11-20 08:51:23 -08:00
response = 500
content_type = 'text/html; charset=utf-8'
2010-11-20 08:51:23 -08:00
if output == '':
error = '<h1>Internal server error</h1>'
2010-11-20 08:51:23 -08:00
if DEBUG:
error = '<p>The server raised this exception: %s</p>' % \
repr(e)
output = httpserver.get_template('generic/error.html') % \
{'title': 'Internal server error',
'error': error}
2010-11-20 08:51:23 -08:00
finally:
self.send_response(response)
self.send_header('Content-type', content_type)
self.end_headers()
if sys.version_info[0] >= 3:
output = output.encode()
self.wfile.write(output)
def get_index(self):
template = httpserver.get_template('webstats/index.html')
channels = self.db.getChannels()
if len(channels) == 0:
title = _('Stats available for no channels')
elif len(channels) == 1:
title = _('Stats available for a channel:')
else:
title = _('Stats available for channels:')
channels_html = ''
for channel in channels:
channels_html += ('<li><a href="/webstats/global/%s/" title="%s">'
'%s</a></li>') % \
(channel[1:].replace('#', ' '), # Strip the leading #
_('View the stats for the %s channel') % channel,
channel)
return template % {'title': title, 'channels': channels_html}
def get_global(self, urlLevel, channel, page, orderby=None):
template = httpserver.get_template('webstats/global.html')
channel = '#' + channel
items = self.db.getChanGlobalData(channel)
bound = self.db.getChanRecordingTimeBoundaries(channel)
hourly_items = self.db.getChanXXlyData(channel, 'hour')
replacement = {'channel': channel,
'escaped_channel': channel[1:].replace('#', ' '),
'quick_stats': utils.str.format(
'%n, %n, %n, %n, %n, %n, %n, and %n.',
(items[0], _('line')), (items[1], _('word')),
(items[2], _('char')), (items[3], _('join')),
(items[4], _('part')), (items[5], _('quit')),
(items[6], _('nick change')),
(items[8], _('kick'))),
'table': getTable(_('Hour'), hourly_items, channel, urlLevel,
page, orderby)[0]
}
return template % replacement
def get_nicks(self, urlLevel, channel, page, orderby=None):
channel = '#' + channel
template = httpserver.get_template('webstats/nicks.html')
items = self.db.getChanGlobalData(channel)
bound = self.db.getChanRecordingTimeBoundaries(channel)
nickly_items = self.db.getChanNickGlobalData(channel, 20)
table, nbItems = getTable(_('Nick'), nickly_items, channel,
urlLevel, page, orderby)
page = int(page)
pagination = ''
if nbItems >= 25:
if page == 0:
pagination += '1 '
else:
pagination += '<a href="0.htm">1</a> '
if page > 100:
pagination += '... '
for i in range(int(max(1,page/25-3)),int(min(nbItems/25-1,page/25+3))):
if page != i*25-1:
pagination += '<a href="%i.htm">%i</a> ' % (i*25-1, i*25)
else:
pagination += '%i ' % (i*25)
if nbItems - page > 100:
pagination += '... '
if page == nbItems-24-1:
pagination += '%i' % (nbItems-24)
else:
pagination += '<a href="%i.htm">%i</a>' % (nbItems-24-1, nbItems-24)
replacement = {
'channel': channel,
'escaped_channel': channel[1:].replace('#', ' '),
'table': table,
'pagination': pagination,
}
return template % replacement
def get_links(self, urlLevel, channel, page, orderby=None):
import pygraphviz
cache = world.webStatsCacheLinks
channel = '#' + channel
items = self.db.getChanLinks(channel)
output = ''
if channel in cache and cache[channel][0] > time.time() - 3600:
output = cache[channel][1]
else:
graph = pygraphviz.AGraph(strict=False, directed=True,
start='regular', smoothing='spring',
size='40') # /!\ Size is in inches /!\
items = [(x,y,float(z)) for x,y,z in items]
if not items:
graph.add_node('No links for the moment.')
buffer_ = BytesIO()
graph.draw(buffer_, prog='circo', format='png')
buffer_.seek(0)
output = buffer_.read()
return output
graph.add_node('#root#', style='invisible')
insertedNicks = {}
divideBy = max([z for x,y,z in items])/10
for item in items:
for i in (0, 1):
if item[i] not in insertedNicks:
try:
insertedNicks.update({item[i]: chooseColor(item[i])})
graph.add_node(item[i], color=insertedNicks[item[i]],
fontcolor=insertedNicks[item[i]])
graph.add_edge(item[i], '#root#', style='invisible',
arrowsize=0, color='white')
except: # Probably unicode issue
pass
graph.add_edge(item[0], item[1], arrowhead='vee',
color=insertedNicks[item[1]],
penwidth=item[2]/divideBy,
arrowsize=item[2]/divideBy/2+1)
buffer_ = BytesIO()
graph.draw(buffer_, prog='circo', format='png')
buffer_.seek(0)
output = buffer_.read()
cache.update({channel: (time.time(), output)})
return output
#####################################################################
# Database
#####################################################################
2010-11-20 08:51:23 -08:00
class WebStatsDB:
def __init__(self):
filename = conf.supybot.directories.data.dirize('WebStats.db')
alreadyExists = os.path.exists(filename)
if alreadyExists and testing:
2010-11-20 08:51:23 -08:00
os.remove(filename)
alreadyExists = False
self._conn = sqlite3.connect(filename, check_same_thread = False)
self._conn.text_factory = str
2010-11-20 08:51:23 -08:00
if not alreadyExists:
self.makeDb()
def makeDb(self):
"""Create the tables in the database"""
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""CREATE TABLE messages (
chan VARCHAR(128),
nick VARCHAR(128),
time TIMESTAMP,
content TEXT
)""")
cursor.execute("""CREATE TABLE moves (
chan VARCHAR(128),
nick VARCHAR(128),
time TIMESTAMP,
2010-11-22 10:07:47 -08:00
type VARCHAR(16),
2010-11-20 08:51:23 -08:00
content TEXT
)""")
2011-03-11 08:59:33 -08:00
cursor.execute("""CREATE TABLE links_cache (
chan VARCHAR(128),
`from` VARCHAR(128),
`to` VARCHAR(128),
`count` VARCHAR(128))""")
cacheTableCreator = """CREATE TABLE %s_cache (
chan VARCHAR(128),
%s
year INT,
month TINYINT,
day TINYINT,
dayofweek TINYINT,
hour TINYINT,
2010-11-20 08:51:23 -08:00
lines INTEGER,
words INTEGER,
chars INTEGER,
joins INTEGER,
parts INTEGER,
2010-11-22 10:07:47 -08:00
quits INTEGER,
nicks INTEGER,
kickers INTEGER,
kickeds INTEGER
)"""
cursor.execute(cacheTableCreator % ('chans', ''))
2010-11-22 08:42:34 -08:00
cursor.execute(cacheTableCreator % ('nicks', 'nick VARCHAR(128),'))
2010-11-20 08:51:23 -08:00
self._conn.commit()
cursor.close()
def getChannels(self):
"""Get a list of channels in the database"""
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""SELECT DISTINCT(chan) FROM chans_cache""")
2010-11-20 08:51:23 -08:00
results = []
for row in cursor:
results.append(row[0])
cursor.close()
return results
def recordMessage(self, chan, nick, message):
"""Called by doPrivmsg or onNotice.
Stores the message in the database"""
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""INSERT INTO messages VALUES (?,?,?,?)""",
(chan, nick, time.time(), message))
self._conn.commit()
cursor.close()
if DEBUG or random.randint(0,50) == 10:
2010-11-20 08:51:23 -08:00
self.refreshCache()
def recordMove(self, chan, nick, type_, message=''):
"""Called by doJoin, doPart, or doQuit.
Stores the 'move' in the database"""
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""INSERT INTO moves VALUES (?,?,?,?,?)""",
(chan, nick, time.time(), type_, message))
self._conn.commit()
cursor.close()
if DEBUG or random.randint(0,50) == 10:
2010-11-20 08:51:23 -08:00
self.refreshCache()
2014-03-23 13:41:30 -07:00
_regexpAddressedTo = re.compile('^(?P<nick>[^:, ]+)[:,]')
2010-11-20 08:51:23 -08:00
def refreshCache(self):
"""Clears the cache tables, and populate them"""
self._truncateCache()
2010-11-20 08:51:23 -08:00
tmp_chans_cache = {}
tmp_nicks_cache = {}
2011-03-11 08:59:33 -08:00
tmp_links_cache = {}
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""SELECT * FROM messages""")
for row in cursor:
chan, nick, timestamp, content = row
chanindex, nickindex = self._getIndexes(chan, nick, timestamp)
self._incrementTmpCache(tmp_chans_cache, chanindex, content)
self._incrementTmpCache(tmp_nicks_cache, nickindex, content)
2011-03-11 08:59:33 -08:00
matched = self._regexpAddressedTo.match(content)
if matched is not None:
to = matched.group('nick')
if chan not in tmp_links_cache:
tmp_links_cache.update({chan: {}})
if nick not in tmp_links_cache[chan]:
tmp_links_cache[chan].update({nick: {}})
if to not in tmp_links_cache[chan][nick]:
tmp_links_cache[chan][nick].update({to: 0})
tmp_links_cache[chan][nick][to] += 1
for chan, nicks in list(tmp_links_cache.items()):
for nick, tos in list(nicks.items()): # Yes, tos is the plural for to
for to, count in list(tos.items()):
2011-03-11 09:58:57 -08:00
if to not in nicks:
continue
2011-03-11 08:59:33 -08:00
cursor.execute('INSERT INTO links_cache VALUES(?,?,?,?)',
(chan, nick, to, count))
2010-11-20 08:51:23 -08:00
cursor.close()
cursor = self._conn.cursor()
cursor.execute("""SELECT * FROM moves""")
for row in cursor:
chan, nick, timestamp, type_, content = row
chanindex, nickindex = self._getIndexes(chan, nick, timestamp)
self._addKeyInTmpCacheIfDoesNotExist(tmp_chans_cache, chanindex)
self._addKeyInTmpCacheIfDoesNotExist(tmp_nicks_cache, nickindex)
2010-11-22 10:07:47 -08:00
id = {'join':3,'part':4,'quit':5,'nick':6,'kicker':7,'kicked':8}
id = id[type_]
tmp_chans_cache[chanindex][id] += 1
tmp_nicks_cache[nickindex][id] += 1
2010-11-20 08:51:23 -08:00
cursor.close()
self._writeTmpCacheToCache(tmp_chans_cache, 'chan')
self._writeTmpCacheToCache(tmp_nicks_cache, 'nick')
self._conn.commit()
def _addKeyInTmpCacheIfDoesNotExist(self, tmpCache, key):
"""Takes a temporary cache list and key.
If the key is not in the list, add it in the list with value list
filled with zeros."""
if key not in tmpCache:
2010-11-22 10:07:47 -08:00
tmpCache.update({key: [0, 0, 0, 0, 0, 0, 0, 0, 0]})
def _truncateCache(self):
"""Clears the cache tables"""
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""DELETE FROM chans_cache""")
cursor.execute("""DELETE FROM nicks_cache""")
2011-03-11 08:59:33 -08:00
cursor.execute("""DELETE FROM links_cache""")
2010-11-20 08:51:23 -08:00
cursor.close()
def _incrementTmpCache(self, tmpCache, index, content):
"""Takes a temporary cache list, the index it'll increment, and the
message content.
Updates the temporary cache to count the content."""
self._addKeyInTmpCacheIfDoesNotExist(tmpCache, index)
tmpCache[index][0] += 1
tmpCache[index][1] += len(content.split(' '))
tmpCache[index][2] += len(content)
def _getIndexes(self, chan, nick, timestamp):
"""Takes a chan name, a nick, and a timestamp, and returns two index,
to crawl the temporary chans and nicks caches."""
dt = datetime.datetime.today()
2010-11-21 01:36:12 -08:00
dt = dt.fromtimestamp(timestamp)
2010-11-28 01:56:58 -08:00
chanindex = (chan,dt.year,dt.month,dt.day,dt.weekday(),dt.hour)
nickindex = (chan,nick,dt.year,dt.month,dt.day,dt.weekday(),dt.hour)
return chanindex, nickindex
def _writeTmpCacheToCache(self, tmpCache, type_):
"""Takes a temporary cache list, its type, and write it in the cache
database."""
cursor = self._conn.cursor()
for index in tmpCache:
data = tmpCache[index]
values = index + tuple(data)
cursor.execute("""INSERT INTO %ss_cache
VALUES(%s)""" % (type_, ('?,'*len(values))[0:-1]), values)
cursor.close()
2010-11-20 08:51:23 -08:00
def getChanGlobalData(self, chanName):
"""Returns a tuple, containing the channel stats, on all the recording
period."""
2010-11-20 08:51:23 -08:00
cursor = self._conn.cursor()
cursor.execute("""SELECT SUM(lines), SUM(words), SUM(chars),
2010-11-22 10:07:47 -08:00
SUM(joins), SUM(parts), SUM(quits),
SUM(nicks), SUM(kickers), SUM(kickeds)
FROM chans_cache WHERE chan=?""", (chanName,))
2010-11-20 08:51:23 -08:00
row = cursor.fetchone()
2010-11-22 10:07:47 -08:00
if None in row:
oldrow = row
row = None
for item in oldrow:
if row is None:
row = (0,)
else:
row += (0,)
assert None not in row
return row
2010-11-20 08:51:23 -08:00
2010-11-21 01:36:12 -08:00
def getChanRecordingTimeBoundaries(self, chanName):
"""Returns two tuples, containing the min and max values of each
year/month/day/dayofweek/hour field.
Note that this data comes from the cache, so they might be a bit
outdated if DEBUG is False."""
cursor = self._conn.cursor()
cursor.execute("""SELECT MIN(year), MIN(month), MIN(day),
MIN(dayofweek), MIN(hour)
FROM chans_cache WHERE chan=?""", (chanName,))
min_ = cursor.fetchone()
cursor = self._conn.cursor()
cursor.execute("""SELECT MAX(year), MAX(month), MAX(day),
MAX(dayofweek), MAX(hour)
FROM chans_cache WHERE chan=?""", (chanName,))
max_ = cursor.fetchone()
if None in min_:
min_ = tuple([int('0') for x in max_])
if None in max_:
max_ = tuple([int('0') for x in max_])
assert None not in min_
assert None not in max_
2010-11-21 01:36:12 -08:00
return min_, max_
def getChanXXlyData(self, chanName, type_):
"""Same as getChanGlobalData, but for the given
year/month/day/dayofweek/hour.
For example, getChanXXlyData('#test', 'hour') returns a list of 24
getChanGlobalData-like tuples."""
sampleQuery = """SELECT SUM(lines), SUM(words), SUM(chars),
2010-11-27 04:20:22 -08:00
SUM(joins), SUM(parts), SUM(quits),
SUM(nicks), SUM(kickers), SUM(kickeds)
FROM chans_cache WHERE chan=? and %s=?"""
min_, max_ = self.getChanRecordingTimeBoundaries(chanName)
typeToIndex = {"year":0, "month":1, "day":2, "dayofweek":3, "hour":4}
if type_ not in typeToIndex:
raise ValueError("Invalid type")
min_ = min_[typeToIndex[type_]]
max_ = max_[typeToIndex[type_]]
results = {}
for index in range(min_, max_+1):
query = sampleQuery % (type_)
cursor = self._conn.cursor()
cursor.execute(query, (chanName, index))
try:
row = cursor.fetchone()
2010-11-27 04:20:22 -08:00
assert row is not None
if None in row:
row=tuple([0 for x in range(0,len(row))])
results.update({index: row})
except:
self._addKeyInTmpCacheIfDoesNotExist(results, index)
cursor.close()
assert None not in results
return results
def getChanNickGlobalData(self, chanName, nick):
"""Same as getChanGlobalData, but only for one nick."""
cursor = self._conn.cursor()
2010-11-27 05:37:07 -08:00
cursor.execute("""SELECT nick, lines, words, chars, joins, parts,
quits, nicks, kickers, kickeds
FROM nicks_cache WHERE chan=?""", (chanName,))
results = {}
for row in cursor:
if row[0] not in results:
2010-11-27 05:37:07 -08:00
results.update({row[0]: row[1:]})
else:
results.update({row[0]: tuple(sum(i)
for i in zip(row[1:], results[row[0]]))})
return results
2011-03-11 08:59:33 -08:00
def getChanLinks(self, chanName):
cursor = self._conn.cursor()
cursor.execute("""SELECT `from`, `to`, `count` FROM links_cache
WHERE chan=?""", (chanName,))
return cursor
2012-09-03 01:58:04 -07:00
def clearChannel(self, channel):
cursor = self._conn.cursor()
for table in ('messages', 'moves', 'links_cache', 'chans_cache',
'nicks_cache'):
cursor.execute('DELETE FROM %s WHERE chan=?' % table, (channel,))
#####################################################################
# Plugin
#####################################################################
2010-11-19 11:06:38 -08:00
class WebStats(callbacks.Plugin):
def __init__(self, irc):
self.__parent = super(WebStats, self)
callbacks.Plugin.__init__(self, irc)
2010-11-22 08:28:55 -08:00
self.lastmsg = {}
self.ircstates = {}
2010-11-20 08:51:23 -08:00
self.db = WebStatsDB()
callback = WebStatsServerCallback()
callback.plugin = self
callback.db = self.db
httpserver.hook('webstats', callback)
2010-11-19 11:06:38 -08:00
def die(self):
httpserver.unhook('webstats')
2010-11-19 11:06:38 -08:00
self.__parent.die()
2012-09-03 01:58:04 -07:00
def clear(self, irc, msg, args, channel, optlist):
"""[<channel>]
Clear database for the <channel>. If <channel> is not given,
it defaults to the current channel."""
capability = ircdb.makeChannelCapability(channel, 'op')
if not ircdb.checkCapability(msg.prefix, capability):
irc.errorNoCapability(capability, Raise=True)
if not optlist:
irc.reply(_('Running this command will wipe all webstats data '
'for the channel. If you are sure you want to do this, '
'add the --confirm switch.'))
return
self.db.clearChannel(channel)
irc.replySuccess()
clear = wrap(clear, ['channel', getopts({'confirm': ''})])
2010-11-20 08:51:23 -08:00
def doPrivmsg(self, irc, msg):
channel = msg.args[0]
if not channel.startswith('#'):
return
if channel == 'AUTH':
return
2010-11-20 08:51:23 -08:00
if not self.registryValue('channel.enable', channel):
return
content = msg.args[1]
nick = msg.prefix.split('!')[0]
if nick in self.registryValue('channel.excludenicks', channel) \
.split(' '):
return
2010-11-20 08:51:23 -08:00
self.db.recordMessage(channel, nick, content)
doNotice = doPrivmsg
2010-11-19 11:06:38 -08:00
def doJoin(self, irc, msg):
channel = msg.args[0]
if not self.registryValue('channel.enable', channel):
return
nick = msg.prefix.split('!')[0]
if nick in self.registryValue('channel.excludenicks', channel) \
.split(' '):
return
self.db.recordMove(channel, nick, 'join')
def doPart(self, irc, msg):
channel = msg.args[0]
if not self.registryValue('channel.enable', channel):
return
if len(msg.args) > 1:
message = msg.args[1]
else:
message = ''
nick = msg.prefix.split('!')[0]
if nick in self.registryValue('channel.excludenicks', channel) \
.split(' '):
return
self.db.recordMove(channel, nick, 'part', message)
def doQuit(self, irc, msg):
nick = msg.prefix.split('!')[0]
if len(msg.args) > 1:
message = msg.args[1]
else:
message = ''
2010-11-22 08:28:55 -08:00
for channel in self.ircstates[irc].channels:
if nick in self.registryValue('channel.excludenicks', channel) \
.split(' '):
continue
2010-11-22 09:23:09 -08:00
if self.registryValue('channel.enable', channel) and \
msg.nick in self.ircstates[irc].channels[channel].users:
2010-11-22 08:28:55 -08:00
self.db.recordMove(channel, nick, 'quit', message)
2010-11-22 10:07:47 -08:00
def doNick(self, irc, msg):
nick = msg.prefix.split('!')[0]
if len(msg.args) > 1:
message = msg.args[1]
else:
message = ''
for channel in self.ircstates[irc].channels:
if nick in self.registryValue('channel.excludenicks', channel) \
.split(' '):
continue
2010-11-22 10:07:47 -08:00
if self.registryValue('channel.enable', channel) and \
msg.nick in self.ircstates[irc].channels[channel].users:
self.db.recordMove(channel, nick, 'nick', message)
def doKick(self, irc, msg):
nick = msg.prefix.split('!')[0]
if len(msg.args) > 1:
message = msg.args[1]
else:
message = ''
for channel in self.ircstates[irc].channels:
if nick in self.registryValue('channel.excludenicks', channel) \
.split(' '):
continue
2010-11-22 10:07:47 -08:00
if self.registryValue('channel.enable', channel) and \
msg.nick in self.ircstates[irc].channels[channel].users:
self.db.recordMove(channel, nick, 'kicker', message)
2010-11-27 05:37:07 -08:00
self.db.recordMove(channel, msg.args[1], 'kicked', message)
2010-11-22 08:28:55 -08:00
def _getLanguage(self, channel):
return self.registryValue('channel.language', '#' + channel)
# The fellowing functions comes from the Relay plugin, provided
2010-11-22 08:28:55 -08:00
# with Supybot
def __call__(self, irc, msg):
try:
irc = self._getRealIrc(irc)
if irc not in self.ircstates:
self._addIrc(irc)
self.ircstates[irc].addMsg(irc, self.lastmsg[irc])
finally:
self.lastmsg[irc] = msg
self.__parent.__call__(irc, msg)
def _addIrc(self, irc):
# Let's just be extra-special-careful here.
if irc not in self.ircstates:
self.ircstates[irc] = irclib.IrcState()
if irc not in self.lastmsg:
self.lastmsg[irc] = ircmsgs.ping('this is just a fake message')
if irc.afterConnect:
# We've probably been reloaded. Let's send some messages to get
# our IrcState objects up to current.
for channel in irc.state.channels:
irc.queueMsg(ircmsgs.who(channel))
irc.queueMsg(ircmsgs.names(channel))
def _getRealIrc(self, irc):
if isinstance(irc, irclib.Irc):
return irc
else:
return irc.getRealIrc()
2010-11-19 11:06:38 -08:00
Class = WebStats
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: