2015-03-05 12:48:10 -05:00
|
|
|
import re
|
2015-06-02 09:13:08 -04:00
|
|
|
import glob
|
2015-05-12 15:38:34 -04:00
|
|
|
import json
|
2015-06-08 15:25:37 -04:00
|
|
|
import logging
|
2015-07-17 09:08:12 -04:00
|
|
|
import argparse
|
2015-06-08 15:25:37 -04:00
|
|
|
import datetime
|
2015-06-23 15:55:12 -04:00
|
|
|
from os import sep
|
2015-03-05 12:48:10 -05:00
|
|
|
import configparser
|
2015-06-08 15:25:37 -04:00
|
|
|
from sys import argv
|
|
|
|
from time import mktime
|
2015-06-23 15:55:12 -04:00
|
|
|
from os.path import exists
|
2015-04-16 13:08:59 -04:00
|
|
|
from jinja2 import Environment, FileSystemLoader
|
|
|
|
|
2015-06-08 15:25:37 -04:00
|
|
|
|
2015-07-17 08:38:57 -04:00
|
|
|
class Exceptions:
|
|
|
|
class ConfigNotFound(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class InvalidConfig(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
class InvalidLog(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
exceptions = Exceptions
|
|
|
|
|
|
|
|
|
2015-06-08 15:25:37 -04:00
|
|
|
class Clients:
|
|
|
|
|
2015-07-13 15:00:58 -04:00
|
|
|
def __init__(self, ident_map={}):
|
2015-06-08 15:25:37 -04:00
|
|
|
self.clients_by_id = {}
|
|
|
|
self.clients_by_uid = {}
|
2015-07-13 15:00:58 -04:00
|
|
|
self.ident_map = ident_map
|
2015-06-08 15:25:37 -04:00
|
|
|
|
|
|
|
def is_id(self, id_or_uid):
|
|
|
|
try:
|
|
|
|
int(id_or_uid)
|
|
|
|
return True
|
|
|
|
except ValueError:
|
|
|
|
return False
|
|
|
|
|
|
|
|
def __add__(self, id_or_uid):
|
|
|
|
if self.is_id(id_or_uid):
|
|
|
|
if id_or_uid not in self.clients_by_id:
|
|
|
|
self.clients_by_id[id_or_uid] = Client(id_or_uid)
|
|
|
|
else:
|
|
|
|
if id_or_uid not in self.clients_by_uid:
|
|
|
|
self.clients_by_uid[id_or_uid] = Client(id_or_uid)
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __getitem__(self, id_or_uid):
|
2015-07-31 07:09:08 -04:00
|
|
|
if id_or_uid in self.ident_map:
|
|
|
|
id_or_uid = self.ident_map[id_or_uid]
|
2015-06-08 15:25:37 -04:00
|
|
|
if self.is_id(id_or_uid):
|
|
|
|
if id_or_uid not in self.clients_by_id:
|
|
|
|
self += id_or_uid
|
|
|
|
return self.clients_by_id[id_or_uid]
|
|
|
|
else:
|
|
|
|
if id_or_uid not in self.clients_by_uid:
|
|
|
|
self += id_or_uid
|
|
|
|
return self.clients_by_uid[id_or_uid]
|
|
|
|
|
|
|
|
|
|
|
|
class Client:
|
|
|
|
|
|
|
|
def __init__(self, identifier):
|
|
|
|
# public
|
|
|
|
self.identifier = identifier
|
|
|
|
self.nick = None
|
2015-06-22 10:20:20 -04:00
|
|
|
self.connected = 0
|
2015-06-22 15:38:28 -04:00
|
|
|
self.onlinetime = 0
|
2015-06-08 15:25:37 -04:00
|
|
|
self.kicks = 0
|
|
|
|
self.pkicks = 0
|
|
|
|
self.bans = 0
|
|
|
|
self.pbans = 0
|
|
|
|
# private
|
|
|
|
self._last_connect = 0
|
|
|
|
|
|
|
|
def connect(self, timestamp):
|
|
|
|
'''
|
|
|
|
client connects at "timestamp"
|
|
|
|
'''
|
|
|
|
logging.debug('CONNECT {}'.format(str(self)))
|
2015-06-22 10:20:20 -04:00
|
|
|
self.connected += 1
|
2015-06-08 15:25:37 -04:00
|
|
|
self._last_connect = timestamp
|
|
|
|
|
|
|
|
def disconnect(self, timestamp):
|
|
|
|
'''
|
|
|
|
client disconnects at "timestamp"
|
|
|
|
'''
|
|
|
|
logging.debug('DISCONNECT {}'.format(str(self)))
|
|
|
|
if not self.connected:
|
2015-06-22 10:20:20 -04:00
|
|
|
logging.debug('^ disconnect before connect')
|
2015-07-17 08:38:57 -04:00
|
|
|
raise exceptions.InvalidLog('disconnect before connect!')
|
2015-06-22 10:20:20 -04:00
|
|
|
self.connected -= 1
|
2015-06-08 15:25:37 -04:00
|
|
|
session_time = timestamp - self._last_connect
|
|
|
|
self.onlinetime += session_time
|
|
|
|
|
|
|
|
def kick(self, target):
|
|
|
|
'''
|
|
|
|
client kicks "target" (Client-obj)
|
|
|
|
'''
|
|
|
|
logging.debug('KICK {} -> {}'.format(str(self), str(target)))
|
|
|
|
target.pkicks += 1
|
|
|
|
self.kicks += 1
|
|
|
|
|
|
|
|
def ban(self, target):
|
|
|
|
'''
|
|
|
|
client bans "target" (Client-obj)
|
|
|
|
'''
|
|
|
|
logging.debug('BAN {} -> {}'.format(str(self), str(target)))
|
|
|
|
target.pbans += 1
|
|
|
|
self.bans += 1
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
return '<{},{}>'.format(self.identifier, self.nick)
|
|
|
|
|
2015-06-22 11:18:49 -04:00
|
|
|
def __getitem__(self, item):
|
|
|
|
return {
|
|
|
|
'identifier': self.identifier,
|
|
|
|
'nick': self.nick,
|
|
|
|
'connected': self.connected,
|
|
|
|
'onlinetime': self.onlinetime,
|
|
|
|
'kicks': self.kicks,
|
|
|
|
'pkicks': self.pkicks,
|
|
|
|
'bans': self.bans,
|
|
|
|
'pbans': self.pbans,
|
|
|
|
}[item]
|
|
|
|
|
2015-03-05 12:48:10 -05:00
|
|
|
|
2015-06-08 15:25:37 -04:00
|
|
|
re_dis_connect = re.compile(r"'(.*)'\(id:(\d*)\)")
|
2015-08-07 09:35:32 -04:00
|
|
|
re_disconnect_invoker = re.compile(
|
|
|
|
r"invokername=(.*)\ invokeruid=(.*)\ reasonmsg"
|
|
|
|
)
|
2015-07-13 14:35:37 -04:00
|
|
|
path_split = __file__.split(sep)[:-1]
|
|
|
|
abspath = sep.join(path_split)
|
|
|
|
if len(path_split) > 0:
|
|
|
|
abspath += sep
|
2015-07-13 14:35:01 -04:00
|
|
|
|
2015-03-05 12:48:10 -05:00
|
|
|
|
2015-07-17 08:38:57 -04:00
|
|
|
def gen_abspath(filename):
|
|
|
|
return filename if filename.startswith(sep) else abspath + filename
|
|
|
|
|
|
|
|
|
2015-07-13 14:47:26 -04:00
|
|
|
def _get_sorted(stor, key):
|
|
|
|
clients = stor.values()
|
2015-08-07 09:35:32 -04:00
|
|
|
cl_data = [(client, client[key]) for client in clients if client[key] > 0]
|
|
|
|
return sorted(cl_data, key=lambda data: data[1], reverse=True)
|
2015-07-13 14:47:26 -04:00
|
|
|
|
|
|
|
|
|
|
|
def _format_seconds(seconds):
|
|
|
|
minutes, seconds = divmod(seconds, 60)
|
|
|
|
hours, minutes = divmod(minutes, 60)
|
|
|
|
hours = str(hours) + 'h ' if hours > 0 else ''
|
|
|
|
minutes = str(minutes) + 'm ' if minutes > 0 else ''
|
|
|
|
seconds = str(seconds) + 's' if seconds > 0 else ''
|
|
|
|
return hours + minutes + seconds
|
|
|
|
|
|
|
|
|
2015-07-13 15:00:58 -04:00
|
|
|
def parse_logs(log_path, ident_map={}, file_log=False):
|
|
|
|
clients = Clients(ident_map)
|
2015-07-13 14:35:01 -04:00
|
|
|
# setup logging
|
|
|
|
log = logging.getLogger()
|
|
|
|
log.setLevel(logging.DEBUG)
|
|
|
|
if file_log:
|
|
|
|
# file logger
|
|
|
|
file_handler = logging.FileHandler('debug.txt', 'w', 'UTF-8')
|
|
|
|
file_handler.setFormatter(logging.Formatter('%(message)s'))
|
|
|
|
file_handler.setLevel(logging.DEBUG)
|
|
|
|
log.addHandler(file_handler)
|
|
|
|
# stream logger (unused)
|
|
|
|
stream_handler = logging.StreamHandler()
|
|
|
|
stream_handler.setLevel(logging.INFO)
|
|
|
|
log.addHandler(stream_handler)
|
|
|
|
|
|
|
|
# find all log-files and collect lines
|
2015-08-07 09:35:32 -04:00
|
|
|
log_files = [file_name for file_name in glob.glob(log_path)]
|
2015-07-13 14:35:01 -04:00
|
|
|
log_lines = []
|
|
|
|
for log_file in log_files:
|
|
|
|
for line in open(log_file, 'r'):
|
|
|
|
log_lines.append(line)
|
|
|
|
|
|
|
|
# process lines
|
|
|
|
for line in log_lines:
|
|
|
|
parts = line.split('|')
|
2015-08-07 09:35:32 -04:00
|
|
|
log_format = '%Y-%m-%d %H:%M:%S.%f'
|
|
|
|
stripped_time = datetime.datetime.strptime(parts[0], log_format)
|
|
|
|
logdatetime = int(stripped_time.timestamp())
|
2015-07-13 14:35:01 -04:00
|
|
|
data = '|'.join(parts[4:]).strip()
|
|
|
|
if data.startswith('client'):
|
|
|
|
nick, clid = re_dis_connect.findall(data)[0]
|
|
|
|
if data.startswith('client connected'):
|
2015-07-13 15:00:58 -04:00
|
|
|
client = clients[clid]
|
|
|
|
client.nick = nick
|
2015-07-13 14:35:01 -04:00
|
|
|
client.connect(logdatetime)
|
|
|
|
elif data.startswith('client disconnected'):
|
2015-07-13 15:00:58 -04:00
|
|
|
client = clients[clid]
|
|
|
|
client.nick = nick
|
2015-07-13 14:35:01 -04:00
|
|
|
client.disconnect(logdatetime)
|
|
|
|
if 'invokeruid' in data:
|
|
|
|
re_disconnect_data = re_disconnect_invoker.findall(data)
|
|
|
|
invokernick, invokeruid = re_disconnect_data[0]
|
|
|
|
invoker = clients[invokeruid]
|
|
|
|
invoker.nick = invokernick
|
|
|
|
if 'bantime' in data:
|
|
|
|
invoker.ban(client)
|
|
|
|
else:
|
|
|
|
invoker.kick(client)
|
2015-07-13 15:00:58 -04:00
|
|
|
return clients
|
2015-07-13 14:35:01 -04:00
|
|
|
|
|
|
|
|
2015-08-07 09:35:32 -04:00
|
|
|
def render_template(clients, output, template_name='template.html',
|
|
|
|
title='TeamspeakStats', debug=False):
|
2015-07-13 15:02:48 -04:00
|
|
|
# prepare clients
|
2015-07-13 14:47:26 -04:00
|
|
|
clients_onlinetime_ = _get_sorted(clients.clients_by_id, 'onlinetime')
|
2015-08-07 09:35:32 -04:00
|
|
|
clients_onlinetime = [
|
|
|
|
(client, _format_seconds(onlinetime))
|
|
|
|
for client, onlinetime in clients_onlinetime_
|
|
|
|
]
|
2015-07-13 14:47:26 -04:00
|
|
|
|
|
|
|
clients_kicks = _get_sorted(clients.clients_by_uid, 'kicks')
|
|
|
|
clients_pkicks = _get_sorted(clients.clients_by_id, 'pkicks')
|
|
|
|
clients_bans = _get_sorted(clients.clients_by_uid, 'bans')
|
|
|
|
clients_pbans = _get_sorted(clients.clients_by_id, 'pbans')
|
|
|
|
objs = [('Onlinetime', clients_onlinetime), ('Kicks', clients_kicks),
|
|
|
|
('passive Kicks', clients_pkicks),
|
2015-08-07 09:35:32 -04:00
|
|
|
('Bans', clients_bans), ('passive Bans', clients_pbans)]
|
2015-07-13 14:47:26 -04:00
|
|
|
|
2015-07-13 15:02:48 -04:00
|
|
|
# render
|
2015-08-07 09:35:32 -04:00
|
|
|
template_loader = FileSystemLoader(abspath)
|
|
|
|
template = Environment(loader=template_loader).get_template(template_name)
|
2015-07-13 14:47:26 -04:00
|
|
|
with open(output, 'w') as f:
|
|
|
|
f.write(template.render(title=title, objs=objs, debug=debug))
|
2015-07-13 14:35:01 -04:00
|
|
|
|
|
|
|
|
2015-07-13 14:52:57 -04:00
|
|
|
def parse_config(config_path):
|
|
|
|
config = configparser.ConfigParser()
|
|
|
|
config.read(config_path)
|
|
|
|
if 'General' not in config or not \
|
2015-08-07 09:35:32 -04:00
|
|
|
('logfile' in config['General'] and
|
|
|
|
'outputfile' in config['General']):
|
2015-07-17 08:38:57 -04:00
|
|
|
raise exceptions.InvalidConfig
|
2015-07-13 14:52:57 -04:00
|
|
|
|
|
|
|
general = config['General']
|
2015-07-17 08:38:57 -04:00
|
|
|
log_path = gen_abspath(general['logfile'])
|
|
|
|
output_path = gen_abspath(general['outputfile'])
|
2015-07-13 14:52:57 -04:00
|
|
|
debug = general.get('debug', 'false') in ['true', 'True']
|
2015-08-07 09:35:32 -04:00
|
|
|
debug_file = general.get('debugfile', 'false') in ['true', 'True']
|
|
|
|
debug_file = debug_file and debug
|
2015-07-13 14:52:57 -04:00
|
|
|
return log_path, output_path, debug, debug_file
|
|
|
|
|
|
|
|
|
2015-07-17 09:08:12 -04:00
|
|
|
def main(config_path='config.ini', id_map_path='id_map.json'):
|
2015-07-13 14:35:01 -04:00
|
|
|
# check cmdline-args
|
2015-07-17 09:08:12 -04:00
|
|
|
config_path = gen_abspath(config_path)
|
|
|
|
id_map_path = gen_abspath(id_map_path)
|
2015-07-13 14:35:01 -04:00
|
|
|
|
|
|
|
if not exists(config_path):
|
2015-07-17 08:38:57 -04:00
|
|
|
raise exceptions.ConfigNotFound(config_path)
|
2015-07-13 14:35:01 -04:00
|
|
|
|
|
|
|
if exists(id_map_path):
|
|
|
|
# read id_map
|
|
|
|
id_map = json.load(open(id_map_path))
|
|
|
|
else:
|
|
|
|
id_map = {}
|
|
|
|
|
2015-07-13 14:52:57 -04:00
|
|
|
log_path, output_path, debug, debug_file = parse_config(config_path)
|
2015-08-07 09:35:32 -04:00
|
|
|
clients = parse_logs(log_path, ident_map=id_map, file_log=debug_file)
|
|
|
|
render_template(clients, output=output_path, debug=debug)
|
2015-07-13 14:35:01 -04:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
2015-08-07 09:35:32 -04:00
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description='A simple Teamspeak stats-generator - based on server-logs'
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--config', type=str, help='path to config', default='config.ini'
|
|
|
|
)
|
|
|
|
parser.add_argument(
|
|
|
|
'--idmap', type=str, help='path to id_map', default='id_map.json'
|
|
|
|
)
|
2015-07-17 09:08:12 -04:00
|
|
|
args = parser.parse_args()
|
|
|
|
main(args.config, args.idmap)
|