TeamspeakStats/tsstats.py

302 lines
9.8 KiB
Python
Raw Normal View History

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-09-04 15:58:42 -04:00
from time import strftime, localtime
2015-06-23 15:55:12 -04:00
from os.path import exists
from jinja2 import Environment, FileSystemLoader
2015-06-08 15:25:37 -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:
def __init__(self, ident_map={}):
2015-06-08 15:25:37 -04:00
self.clients_by_id = {}
self.clients_by_uid = {}
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):
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]
2015-08-26 06:44:56 -04:00
def __iter__(self):
for id_client in self.clients_by_id.values():
yield id_client
for uid_client in self.clients_by_uid.values():
yield uid_client
2015-06-08 15:25:37 -04:00
class Client:
def __init__(self, identifier):
# public
self.identifier = identifier
self.nick = None
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
2015-09-04 15:49:10 -04:00
self.last_seen = 0
2015-06-08 15:25:37 -04:00
# private
self._last_connect = 0
def connect(self, timestamp):
'''
client connects at "timestamp"
'''
logging.debug('CONNECT {}'.format(str(self)))
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:
logging.debug('^ disconnect before connect')
raise exceptions.InvalidLog('disconnect before connect!')
self.connected -= 1
2015-06-08 15:25:37 -04:00
session_time = timestamp - self._last_connect
self.onlinetime += session_time
2015-09-04 15:49:10 -04:00
self.last_seen = timestamp
2015-06-08 15:25:37 -04:00
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*)\)")
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-03-05 12:48:10 -05:00
def gen_abspath(filename):
return filename if filename.startswith(sep) else abspath + filename
def _get_sorted(stor, key):
clients = stor.values()
cl_data = [(client, client[key]) for client in clients if client[key] > 0]
return sorted(cl_data, key=lambda data: data[1], reverse=True)
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
def parse_logs(log_path, ident_map={}, file_log=False):
clients = Clients(ident_map)
# 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 open them
2015-09-04 16:01:18 -04:00
file_paths = sorted([file_path for file_path in glob.glob(log_path)])
2015-09-04 16:01:18 -04:00
for file_path in file_paths:
log_file = open(file_path)
# process lines
logging.debug('Started parsing of {}'.format(log_file.name))
for line in log_file:
parts = line.split('|')
log_format = '%Y-%m-%d %H:%M:%S.%f'
stripped_time = datetime.datetime.strptime(parts[0], log_format)
logdatetime = int(stripped_time.timestamp())
data = '|'.join(parts[4:]).strip()
if data.startswith('client'):
nick, clid = re_dis_connect.findall(data)[0]
if data.startswith('client connected'):
client = clients[clid]
client.nick = nick
client.connect(logdatetime)
elif data.startswith('client disconnected'):
client = clients[clid]
client.nick = nick
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)
logging.debug('Finished parsing of {}'.format(log_file.name))
return clients
def render_template(clients, output, template_name='template.html',
title='TeamspeakStats', debug=False):
# prepare clients
clients_onlinetime_ = _get_sorted(clients.clients_by_id, 'onlinetime')
clients_onlinetime = [
(client, _format_seconds(onlinetime))
for client, onlinetime in clients_onlinetime_
]
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),
('Bans', clients_bans), ('passive Bans', clients_pbans)]
# render
template_loader = FileSystemLoader(abspath)
2015-09-04 15:58:42 -04:00
template_env = Environment(loader=template_loader)
def frmttime(timestamp):
return strftime('%x %X', localtime(int(timestamp)))
template_env.filters['frmttime'] = frmttime
template = template_env.get_template(template_name)
with open(output, 'w') as f:
f.write(template.render(title=title, objs=objs, debug=debug))
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 \
('logfile' in config['General'] and
'outputfile' in config['General']):
raise exceptions.InvalidConfig
2015-07-13 14:52:57 -04:00
general = config['General']
log_path = gen_abspath(general['logfile'])
output_path = gen_abspath(general['outputfile'])
return log_path, output_path
2015-07-13 14:52:57 -04:00
def main(config_path='config.ini', id_map_path='id_map.json',
debug=False, debugfile=False):
# 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)
if not exists(config_path):
raise exceptions.ConfigNotFound(config_path)
if exists(id_map_path):
# read id_map
id_map = json.load(open(id_map_path))
else:
id_map = {}
log_path, output_path = parse_config(config_path)
clients = parse_logs(log_path, ident_map=id_map, file_log=debugfile)
render_template(clients, output=output_path, debug=debug)
if __name__ == '__main__':
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'
)
parser.add_argument(
'--debug', help='debug mode', action='store_true'
)
parser.add_argument(
'--debugfile', help='write debug-log to file', action='store_true'
)
2015-07-17 09:08:12 -04:00
args = parser.parse_args()
main(args.config, args.idmap, args.debug, args.debugfile)