WebStats: make it works basically
parent
2c6b022a7d
commit
37b4c7eb04
|
@ -30,9 +30,13 @@
|
|||
|
||||
import supybot.conf as conf
|
||||
import supybot.registry as registry
|
||||
from supybot.i18n import PluginInternationalization, internationalizeDocstring
|
||||
|
||||
_ = PluginInternationalization('WebStats')
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
from supybot.i18n import internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
except:
|
||||
_ = lambda x:x
|
||||
internationalizeDocstring = lambda x:x
|
||||
|
||||
def configure(advanced):
|
||||
# This will be called by supybot to configure this module. advanced is
|
||||
|
@ -55,4 +59,9 @@ conf.registerGlobalValue(WebStats.server, 'port',
|
|||
registry.Integer(8080, _("""Determines the port the web server will
|
||||
bind.""")))
|
||||
|
||||
conf.registerGroup(WebStats, 'channel')
|
||||
conf.registerChannelValue(WebStats.channel, 'enable',
|
||||
registry.Boolean(False, _("""Determines whether the stats are enabled
|
||||
for this channel.""")))
|
||||
|
||||
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
|
||||
|
|
|
@ -28,42 +28,217 @@
|
|||
|
||||
###
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
import BaseHTTPServer
|
||||
import WebStats.templates as templates
|
||||
import supybot.conf as conf
|
||||
import supybot.world as world
|
||||
import supybot.utils as utils
|
||||
from supybot.commands import *
|
||||
import supybot.plugins as plugins
|
||||
import supybot.ircutils as ircutils
|
||||
import supybot.callbacks as callbacks
|
||||
import supybot.i18n as i18n
|
||||
from supybot.i18n import PluginInternationalization, internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
|
||||
class TemplateOpeningError(Exception):
|
||||
pass
|
||||
try:
|
||||
import sqlite3
|
||||
except ImportError:
|
||||
from pysqlite2 import dbapi2 as sqlite3 # for python2.4
|
||||
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
from supybot.i18n import internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
except:
|
||||
_ = lambda x:x
|
||||
internationalizeDocstring = lambda x:x
|
||||
|
||||
DEBUG = True
|
||||
|
||||
testing = world.testing
|
||||
|
||||
def getTemplate(name):
|
||||
if sys.modules.has_key('WebStats.templates.skeleton'):
|
||||
reload(sys.modules['WebStats.templates.skeleton'])
|
||||
if sys.modules.has_key('WebStats.templates.%s' % name):
|
||||
reload(sys.modules['WebStats.templates.%s' % name])
|
||||
module = __import__('WebStats.templates.%s' % name)
|
||||
print repr(module)
|
||||
return module
|
||||
|
||||
htmlPages = {}
|
||||
return getattr(getattr(module, 'templates'), name)
|
||||
|
||||
class FooException(Exception):
|
||||
pass
|
||||
|
||||
class HTTPHandler(BaseHTTPServer.BaseHTTPRequestHandler):
|
||||
def do_GET(s):
|
||||
if not htmlPages.has_key(s.path):
|
||||
s.send_response(404)
|
||||
s.send_header("Content-type", "text/html")
|
||||
s.end_headers()
|
||||
s.wfile.write(templates.error404.get())
|
||||
def do_GET(self):
|
||||
output = ''
|
||||
try:
|
||||
if self.path == '/design.css':
|
||||
response = 200
|
||||
content_type = 'text/css'
|
||||
output = getTemplate('design').get(not testing)
|
||||
elif self.path == '/':
|
||||
response = 200
|
||||
content_type = 'text/html'
|
||||
output = getTemplate('index').get(not testing,
|
||||
self.server.db.getChannels())
|
||||
elif self.path == '/about/':
|
||||
response = 200
|
||||
content_type = 'text/html'
|
||||
self.end_headers()
|
||||
output = getTemplate('about').get(not testing)
|
||||
elif self.path.startswith('/%s/' % _('channels')):
|
||||
response = 200
|
||||
content_type = 'text/html'
|
||||
chanName = self.path[len(_('channels'))+2:].split('/')[0]
|
||||
output = getTemplate('chan_index').get(not testing, chanName,
|
||||
self.server.db)
|
||||
else:
|
||||
response = 404
|
||||
content_type = 'text/html'
|
||||
output = getTemplate('error404').get(not testing)
|
||||
except Exception as e:
|
||||
response = 500
|
||||
content_type = 'text/html'
|
||||
if output == '':
|
||||
output = '<h1>Internal server error</h1>'
|
||||
if DEBUG:
|
||||
output += '<p>The server raised this exception: %s</p>' % \
|
||||
repr(e)
|
||||
finally:
|
||||
self.send_response(response)
|
||||
self.send_header('Content-type', content_type)
|
||||
self.end_headers()
|
||||
self.wfile.write(output)
|
||||
|
||||
class WebStatsDB:
|
||||
def __init__(self):
|
||||
filename = conf.supybot.directories.data.dirize('WebStats.db')
|
||||
alreadyExists = os.path.exists(filename)
|
||||
if alreadyExists and (DEBUG or testing):
|
||||
os.remove(filename)
|
||||
alreadyExists = False
|
||||
self._conn = sqlite3.connect(filename, check_same_thread = False)
|
||||
if not alreadyExists:
|
||||
self.makeDb()
|
||||
|
||||
def makeDb(self):
|
||||
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,
|
||||
type CHAR(4),
|
||||
content TEXT
|
||||
)""")
|
||||
cursor.execute("""CREATE TABLE chans_cache (
|
||||
chan VARCHAR(128) PRIMARY KEY,
|
||||
lines INTEGER,
|
||||
words INTEGER,
|
||||
chars INTEGER,
|
||||
joins INTEGER,
|
||||
parts INTEGER,
|
||||
quits INTEGER
|
||||
)""")
|
||||
cursor.execute("""CREATE TABLE nicks_cache (
|
||||
nick VARCHAR(128) PRIMARY KEY,
|
||||
lines INTEGER,
|
||||
words INTEGER,
|
||||
chars INTEGER,
|
||||
joins INTEGER,
|
||||
parts INTEGER,
|
||||
quits INTEGER
|
||||
)""")
|
||||
self._conn.commit()
|
||||
cursor.close()
|
||||
|
||||
def getChannels(self):
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""SELECT chan FROM chans_cache""")
|
||||
results = []
|
||||
for row in cursor:
|
||||
results.append(row[0])
|
||||
cursor.close()
|
||||
return results
|
||||
|
||||
def recordMessage(self, chan, nick, message):
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""INSERT INTO messages VALUES (?,?,?,?)""",
|
||||
(chan, nick, time.time(), message))
|
||||
self._conn.commit()
|
||||
cursor.close()
|
||||
if DEBUG:
|
||||
self.refreshCache()
|
||||
|
||||
def recordMove(self, chan, nick, type_, message=''):
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""INSERT INTO moves VALUES (?,?,?,?,?)""",
|
||||
(chan, nick, time.time(), type_, message))
|
||||
self._conn.commit()
|
||||
cursor.close()
|
||||
if DEBUG:
|
||||
self.refreshCache()
|
||||
|
||||
def refreshCache(self):
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""DELETE FROM chans_cache""")
|
||||
cursor.execute("""DELETE FROM nicks_cache""")
|
||||
cursor.close()
|
||||
tmp_chans_cache = {}
|
||||
tmp_nicks_cache = {}
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""SELECT * FROM messages""")
|
||||
for row in cursor:
|
||||
chan, nick, timestamp, content = row
|
||||
if not tmp_chans_cache.has_key(chan):
|
||||
tmp_chans_cache.update({chan: [0, 0, 0, 0, 0, 0]})
|
||||
tmp_chans_cache[chan][0] += 1
|
||||
tmp_chans_cache[chan][1] += len(content.split(' '))
|
||||
tmp_chans_cache[chan][2] += len(content)
|
||||
if not tmp_nicks_cache.has_key(nick):
|
||||
tmp_nicks_cache.update({nick: [0, 0, 0, 0, 0, 0]})
|
||||
tmp_nicks_cache[nick][0] += 1
|
||||
tmp_nicks_cache[nick][1] += len(content.split(' '))
|
||||
tmp_nicks_cache[nick][2] += len(content)
|
||||
cursor.close()
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("""SELECT * FROM moves""")
|
||||
for row in cursor:
|
||||
chan, nick, timestamp, type_, content = row
|
||||
if not tmp_chans_cache.has_key(chan):
|
||||
tmp_chans_cache.update({chan: [0, 0, 0, 0, 0, 0]})
|
||||
id = {'join': 3, 'part': 4, 'quit': 5}[type_]
|
||||
tmp_chans_cache[chan][id] += 1
|
||||
tmp_nicks_cache[nick][id] += 1
|
||||
cursor.close()
|
||||
cursor = self._conn.cursor()
|
||||
for chan in tmp_chans_cache:
|
||||
data = tmp_chans_cache[chan]
|
||||
cursor.execute("INSERT INTO chans_cache VALUES(?,?,?,?,?,?,?)",
|
||||
(chan, data[0], data[1], data[2], data[3],
|
||||
data[4], data[5]))
|
||||
for nick in tmp_nicks_cache:
|
||||
data = tmp_nicks_cache[nick]
|
||||
cursor.execute("INSERT INTO nicks_cache VALUES(?,?,?,?,?,?,?)",
|
||||
(nick, data[0], data[1], data[2], data[3],
|
||||
data[4], data[5]))
|
||||
cursor.close()
|
||||
self._conn.commit()
|
||||
|
||||
def getChanMainData(self, chanName):
|
||||
cursor = self._conn.cursor()
|
||||
cursor.execute("SELECT * FROM chans_cache WHERE chan=?", (chanName,))
|
||||
row = cursor.fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return (str(row[0]),) + row[1:]
|
||||
|
||||
|
||||
class Server:
|
||||
def __init__(self, plugin):
|
||||
|
@ -72,7 +247,16 @@ class Server:
|
|||
def run(self):
|
||||
serverAddress = (self._plugin.registryValue('server.host'),
|
||||
self._plugin.registryValue('server.port'))
|
||||
httpd = BaseHTTPServer.HTTPServer(serverAddress, HTTPHandler)
|
||||
done = False
|
||||
while not done:
|
||||
time.sleep(1)
|
||||
try:
|
||||
httpd = BaseHTTPServer.HTTPServer(serverAddress, HTTPHandler)
|
||||
done = True
|
||||
except:
|
||||
pass
|
||||
print 'WebStats web server launched'
|
||||
httpd.db = self._plugin.db
|
||||
while self.serve:
|
||||
httpd.handle_request()
|
||||
httpd.server_close()
|
||||
|
@ -86,6 +270,7 @@ class WebStats(callbacks.Plugin):
|
|||
def __init__(self, irc):
|
||||
self.__parent = super(WebStats, self)
|
||||
callbacks.Plugin.__init__(self, irc)
|
||||
self.db = WebStatsDB()
|
||||
self._server = Server(self)
|
||||
if not world.testing:
|
||||
threading.Thread(target=self._server.run,
|
||||
|
@ -95,7 +280,14 @@ class WebStats(callbacks.Plugin):
|
|||
self._server.serve = False
|
||||
self.__parent.die()
|
||||
|
||||
|
||||
def doPrivmsg(self, irc, msg):
|
||||
channel = msg.args[0]
|
||||
if not self.registryValue('channel.enable', channel):
|
||||
return
|
||||
content = msg.args[1]
|
||||
nick = msg.prefix.split('!')[0]
|
||||
self.db.recordMessage(channel, nick, content)
|
||||
doNotice = doPrivmsg
|
||||
|
||||
Class = WebStats
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import skeleton
|
||||
import supybot.utils as utils
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
from supybot.i18n import internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
except:
|
||||
_ = lambda x:x
|
||||
internationalizeDocstring = lambda x:x
|
||||
|
||||
content = \
|
||||
"<h1>%s</h1><p>%s</p>"
|
||||
|
||||
def get(useSkeleton, channel, db):
|
||||
channel = '#' + channel
|
||||
items = db.getChanMainData(channel)
|
||||
output = content % (_("Stats about %s channel"), _("There were %n, %n, %n, %n, %n, and %n."))
|
||||
output = utils.str.format(output, channel, (items[1], 'line'), (items[2], 'word'),
|
||||
(items[3], 'char'), (items[4], 'join'),
|
||||
(items[5], 'part'), (items[6], 'quit'))
|
||||
if useSkeleton:
|
||||
output = ''.join([skeleton.start, output, skeleton.end])
|
||||
return output
|
|
@ -0,0 +1,85 @@
|
|||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
from supybot.i18n import internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
except:
|
||||
_ = lambda x:x
|
||||
internationalizeDocstring = lambda x:x
|
||||
|
||||
content = \
|
||||
"""/*
|
||||
Copyright (c) 2010, Valentin Lorentz
|
||||
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.
|
||||
*/
|
||||
|
||||
body, html {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#header {
|
||||
width: 100%;
|
||||
font-size: 1.2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#menu {
|
||||
width: 100%;
|
||||
font-size: 0.8em;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
#menu:before {
|
||||
content: \"""" + _('Menu:') + """\";
|
||||
}
|
||||
|
||||
#menu li {
|
||||
list-style-type: none;
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#footer {
|
||||
width: 100%;
|
||||
font-size: 0.6em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chanslist li {
|
||||
list-style-type: none;
|
||||
}
|
||||
.chanslist li a:visited {
|
||||
color: blue;
|
||||
}
|
||||
"""
|
||||
|
||||
def get(useSkeleton=True):
|
||||
return content
|
|
@ -7,10 +7,8 @@ content = \
|
|||
</p>
|
||||
"""
|
||||
|
||||
content = '%s%s%s' % (skeleton.start, content, skeleton.end)
|
||||
|
||||
print 'toto'
|
||||
|
||||
def get():
|
||||
def get(useSkeleton=True):
|
||||
global content
|
||||
if useSkeleton:
|
||||
content = '%s%s%s' % (skeleton.start, content, skeleton.end)
|
||||
return content
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
import skeleton
|
||||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
from supybot.i18n import internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
except:
|
||||
_ = lambda x:x
|
||||
internationalizeDocstring = lambda x:x
|
||||
|
||||
content = """
|
||||
<h1>%s</h1>
|
||||
"""
|
||||
|
||||
def get(useSkeleton, channels):
|
||||
output = content
|
||||
if len(channels) == 0:
|
||||
output %= _('Stats available for no channels')
|
||||
elif len(channels) == 1:
|
||||
output %= _('Stats available for a channel:')
|
||||
elif len(channels):
|
||||
output %= _('Stats available for channels:')
|
||||
output += '<ul class="chanslist">'
|
||||
for channel in channels:
|
||||
output += '<li><a href="/%s/%s" title="%s">%s</a></li>' % (
|
||||
_('channels'),
|
||||
channel[1:], # Strip the #
|
||||
_('View the stats for the %s channel') % channel,
|
||||
channel)
|
||||
output += '</ul>'
|
||||
if useSkeleton:
|
||||
output = ''.join([skeleton.start, output, skeleton.end])
|
||||
return output
|
|
@ -1,17 +1,35 @@
|
|||
try:
|
||||
from supybot.i18n import PluginInternationalization
|
||||
from supybot.i18n import internationalizeDocstring
|
||||
_ = PluginInternationalization('WebStats')
|
||||
except:
|
||||
_ = lambda x:x
|
||||
internationalizeDocstring = lambda x:x
|
||||
|
||||
start = \
|
||||
"""<!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" xml:lang="fr" >
|
||||
<head>
|
||||
<title>Supybot WebStats</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<link rel="stylesheet" media="screen" type="text/css" title="Design" href="/design.css" />
|
||||
</head>
|
||||
<body>
|
||||
"""
|
||||
<p id="header">
|
||||
WebStats
|
||||
</p>
|
||||
<ul id="menu">
|
||||
<li><a href="/" title="%s">%s</a></li>
|
||||
<li><a href="/%s/" title="%s">%s</a></li>
|
||||
</ul>
|
||||
""" % (_('Come back to the root page'), _('Home'),
|
||||
_('about'), _('Get more informations about this website'), _('About'))
|
||||
|
||||
end = \
|
||||
"""
|
||||
<p id="footer">
|
||||
Supybot WebStats powered.<br />
|
||||
<a href="http://supybot.com">Supybot</a> and
|
||||
<a href="http://supybot-fr.tk/WebStats">WebStats</a> powered.<br />
|
||||
Libre software available under BSD licence.
|
||||
</p>
|
||||
</body>
|
||||
|
|
Loading…
Reference in New Issue