diff --git a/README.md b/README.md
index 808dc3c..887f036 100644
--- a/README.md
+++ b/README.md
@@ -14,11 +14,6 @@ Run `tsstats.py` and point your web-server to the generated .html-file, now you
- outputfile `Path to the location, where the generator will put the generated .html-file`
`[HTML]`
- title `HTML-Title`
-- onlinetime `Show the onlinetime-section (default=True)`
-- kicks `Show the kicks-section (default=True)`
-- pkicks `Show the passive-kicks-section (default=True)`
-- bans `Show the bans-section (default=True)`
-- pbans `Show the passive-bans-section (default=True)`
## Example
@@ -26,9 +21,6 @@ Run `tsstats.py` and point your web-server to the generated .html-file, now you
[General]
logfile = /usr/local/bin/teamspeak-server/logs/ts3server_2015-03-02__14_01_43.110983_1.log
outputfile = /var/www/html/stats.html
-[HTML]
-title = TeamspeakStats
-bans = False
```
# ID-Mapping
diff --git a/template.html b/template.html
index ee621c9..8d0b161 100644
--- a/template.html
+++ b/template.html
@@ -1,73 +1,27 @@
-
-
-
-
- {{ title }}
-
-
- {# Onlinetime #}
- {% if show_onlinetime is sameas true %}
-
Onlinetime
-
- {% for clid, nick, onlinetime_, connected in onlinetime %}
-
{{ nick }}{{ onlinetime_ }}
- {% else %}
-
No data
- {% endfor %}
-
- {% endif %}
-
- {# Kicks #}
- {% if show_kicks is sameas true %}
-
Kicks
-
- {% for _, nick, kicks_ in kicks %}
-
{{ nick }}{{ kicks_ }}
- {% else %}
-
No data
- {% endfor %}
-
- {% endif %}
-
- {# passive Kicks #}
- {% if show_pkicks is sameas true %}
-
passive Kicks
-
- {% for _, nick, pkicks_ in pkicks %}
-
{{ nick }}{{ pkicks_ }}
- {% else %}
-
No data
- {% endfor %}
-
- {% endif %}
-
- {# Bans #}
- {% if show_bans is sameas true %}
-
Bans
-
- {% for _, nick, bans_ in bans %}
-
{{ nick }}{{ bans_ }}
- {% else %}
-
No data
- {% endfor %}
-
- {% endif %}
- {# passive Bans #}
- {% if show_pbans is sameas true %}
-
passive Bans
-
- {% for _, nick, pbans_ in pbans %}
-
{{ nick }}{{ pbans_ }}
- {% else %}
-
No data
- {% endfor %}
-
- {% endif %}
-
generated in {{ seconds }} at {{ date }}
-
+
+ {{ title }}
+
+ {# #}
+
+
+
+
+{% for headline, list in objs %}
+ {% if list|length > 0 %}
+
{{ headline }}
+
+ {% for client, value in list %}
+
{{ client.nick }}{{ value }}
+ {% endfor %}
+
+ {% endif %}
+{% endfor %}
+
generated in {{ generation_time }} seconds at {{ time }}
+
+
diff --git a/tsstats.py b/tsstats.py
index bf4bacc..2f52e3a 100755
--- a/tsstats.py
+++ b/tsstats.py
@@ -1,56 +1,128 @@
import re
-import sys
import glob
import json
+import logging
+import datetime
import configparser
-from os.path import exists
-from telnetlib import Telnet
-from time import mktime, sleep
-from datetime import datetime, timedelta
+from sys import argv
+from time import mktime
+from os.path import exists, abspath
from jinja2 import Environment, FileSystemLoader
+# logging
+log = logging.getLogger()
+log.setLevel(logging.DEBUG)
+# create handler
+file_handler = logging.FileHandler('debug.txt', 'w', 'UTF-8')
+file_handler.setFormatter(logging.Formatter('[%(asctime)s] %(message)s', '%d.%m.%Y %H:%M:%S'))
+file_handler.setLevel(logging.DEBUG)
-def exit(error):
- print('FATAL ERROR:', error)
- import sys
- sys.exit(1)
+stream_handler = logging.StreamHandler()
+stream_handler.setLevel(logging.INFO)
+# add handler
+log.addHandler(file_handler)
+log.addHandler(stream_handler)
-# get path
-arg = sys.argv[0]
-arg_find = arg.rfind('/')
-if arg_find == -1:
- path = '.'
-else:
- path = arg[:arg_find]
-path += '/'
-config_path = path + 'config.ini'
-id_map_path = path + 'id_map.json'
+class Clients:
+
+ def __init__(self):
+ self.clients_by_id = {}
+ self.clients_by_uid = {}
+
+ 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 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]
+
+clients = Clients()
+
+
+class Client:
+
+ def __init__(self, identifier):
+ # public
+ self.identifier = identifier
+ self.nick = None
+ self.connected = False
+ self.onlinetime = datetime.timedelta()
+ 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)))
+ self.connected = True
+ self._last_connect = timestamp
+
+ def disconnect(self, timestamp):
+ '''
+ client disconnects at "timestamp"
+ '''
+ logging.debug('DISCONNECT {}'.format(str(self)))
+ if not self.connected:
+ raise Exception('WTF did just happen?! A client disconnected before connecting!')
+ self.connected = False
+ 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)
+
+ def __format__(self):
+ return self.__str__()
+
+# check cmdline-args
+config_path = argv[1] if len(argv) >= 2 else 'config.ini'
+config_path = abspath(config_path)
+id_map_path = argv[2] if len(argv) >= 3 else 'id_map.json'
+id_map_path = abspath(id_map_path)
-# exists config-file
if not exists(config_path):
- exit('Couldn\'t find config-file at {}'.format(config_path))
-
-# parse config
-config = configparser.ConfigParser()
-config.read(config_path)
-# check keys
-if 'General' not in config or 'HTML' not in config:
- exit('Invalid config!')
-general = config['General']
-html = config['HTML']
-if ('logfile' not in general or 'outputfile' not in general) or ('title' not in html):
- exit('Invalid config!')
-log_path = general['logfile']
-if not exists(log_path):
- exit('Couldn\'t access log-file!')
-output_path = general['outputfile']
-title = html['title']
-show_onlinetime = html.get('onlinetime', True)
-show_kicks = html.get('kicks', True)
-show_pkicks = html.get('pkicks', True)
-show_bans = html.get('bans', True)
-show_pbans = html.get('pbans', True)
+ raise Exception('Couldn\'t find config-file at {}'.format(config_path))
if exists(id_map_path):
# read id_map
@@ -58,176 +130,83 @@ if exists(id_map_path):
else:
id_map = {}
-generation_start = datetime.now()
-clients = {} # clid: {'nick': ..., 'onlinetime': ..., 'kicks': ..., 'pkicks': ..., 'bans': ..., 'last_connect': ..., 'connected': ...}
-kicks = {}
+# parse config
+config = configparser.ConfigParser()
+config.read(config_path)
+# check keys
+if 'General' not in config:
+ raise Exception('Invalid config! Section "General" missing!')
+general = config['General']
+html = config['HTML'] if 'HTML' in config.sections() else {}
+if not ('logfile' in general or 'outputfile' in general):
+ raise Exception('Invalid config! "logfile" and/or "outputfile" missing!')
+log_path = general['logfile']
+if not exists(log_path):
+ raise Exception('Couldn\'t access log-file!')
+output_path = general['outputfile']
+title = html.get('title', 'TeamspeakStats')
-cldata = re.compile(r"'(.*)'\(id:(\d*)\)")
-cldata_ban = re.compile(r"by\ client\ '(.*)'\(id:(\d*)\)")
-cldata_invoker = re.compile(r"invokerid=\d*\ invokername=(.*)\ invokeruid=(.*)\ reasonmsg")
+generation_start = datetime.datetime.now()
+re_dis_connect = re.compile(r"'(.*)'\(id:(\d*)\)")
+re_disconnect_invoker = re.compile(r"invokername=(.*)\ invokeruid=(.*)\ reasonmsg")
-def add_connect(clid, nick, logdatetime):
- check_client(clid, nick)
- clients[clid]['last_connect'] = mktime(logdatetime.timetuple())
- clients[clid]['connected'] = True
-
-
-def add_disconnect(clid, nick, logdatetime, set_connected=True):
- check_client(clid, nick)
- connect = datetime.fromtimestamp(clients[clid]['last_connect'])
- delta = logdatetime - connect
- minutes = delta.seconds // 60
- increase_onlinetime(clid, minutes)
- if set_connected:
- clients[clid]['connected'] = False
-
-
-def add_ban(clid, nick):
- check_client(clid, nick)
- if 'bans' in clients[clid]:
- clients[clid]['bans'] += 1
- else:
- clients[clid]['bans'] = 1
-
-
-def add_pban(clid, nick):
- check_client(clid, nick)
- if 'pbans' in clients[clid]:
- clients[clid]['pbans'] += 1
- else:
- clients[clid]['pbans'] = 1
-
-
-####
-#
-#
-# TODO
-#
-#
-###
-def add_kick(cluid, nick):
- if cluid not in kicks:
- kicks[cluid] = {}
- if 'kicks' in kicks[cluid]:
- kicks[cluid]['kicks'] += 1
- else:
- kicks[cluid]['kicks'] = 1
- kicks[cluid]['nick'] = nick
-
-
-def add_pkick(clid, nick):
- check_client(clid, nick)
- if 'pkicks' in clients[clid]:
- clients[clid]['pkicks'] += 1
- else:
- clients[clid]['pkicks'] = 1
-
-
-def increase_onlinetime(clid, onlinetime):
- if 'onlinetime' in clients[clid]:
- clients[clid]['onlinetime'] += onlinetime
- else:
- clients[clid]['onlinetime'] = onlinetime
-
-
-def check_client(clid, nick):
- if clid not in clients:
- clients[clid] = {}
- clients[clid]['nick'] = nick
-
+# find all log-files and collect lines
log_files = [file_name for file_name in glob.glob(log_path)]
log_lines = []
for log_file in log_files:
for line in open(log_file, 'r'):
log_lines.append(line)
-today = datetime.utcnow()
+
+# process lines
for line in log_lines:
parts = line.split('|')
- logdatetime = datetime.strptime(parts[0], '%Y-%m-%d %H:%M:%S.%f')
- sid = int(parts[3].strip())
- data = '|'.join(parts[4:]).strip()
+ logdatetime = datetime.datetime.strptime(parts[0], '%Y-%m-%d %H:%M:%S.%f')
+ data = parts[4].strip()
if data.startswith('client'):
- r = cldata.findall(data)[0]
- nick = r[0]
- clid = r[1]
+ nick, clid = re_dis_connect.findall(data)[0]
if clid in id_map:
clid = id_map[clid]
+ client = clients[clid]
+ client.nick = nick
if data.startswith('client connected'):
- add_connect(clid, nick, logdatetime)
+ client.connect(logdatetime)
elif data.startswith('client disconnected'):
- add_disconnect(clid, nick, logdatetime)
- if 'bantime' in data:
- add_pban(clid, nick)
- elif 'invokerid' in data:
- add_pkick(clid, nick)
- r = cldata_invoker.findall(data)[0]
- nick = r[0]
- cluid = r[1]
- add_kick(cluid, nick)
- elif data.startswith('ban added') and 'cluid' in data:
- r = cldata_ban.findall(data)[0]
- nick = r[0]
- clid = r[1]
- add_ban(clid, 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)
-for clid in clients:
- if 'connected' not in clients[clid]:
- clients[clid]['connected'] = False
- if clients[clid]['connected']:
- add_disconnect(clid, clients[clid]['nick'], today, set_connected=False)
-
-
-generation_end = datetime.now()
+generation_end = datetime.datetime.now()
generation_delta = generation_end - generation_start
+# render template
+template = Environment(loader=FileSystemLoader(abspath('.'))).get_template('template.html')
+# sort all values desc
+cl_by_id = clients.clients_by_id
+cl_by_uid = clients.clients_by_uid
+clients_onlinetime_ = sorted([(client, client.onlinetime) for client in cl_by_id.values()], key=lambda data: data[1], reverse=True)
+clients_onlinetime = []
+for client, onlinetime in clients_onlinetime_:
+ minutes, seconds = divmod(client.onlinetime.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 ''
+ clients_onlinetime.append((client, hours + minutes + seconds))
+clients_kicks = sorted([(client, client.kicks) for client in cl_by_uid.values() if client.kicks > 0], key=lambda data: data[1], reverse=True)
+clients_pkicks = sorted([(client, client.pkicks) for client in cl_by_id.values() if client.pkicks > 0], key=lambda data: data[1], reverse=True)
+clients_bans = sorted([(client, client.bans) for client in cl_by_uid.values() if client.bans > 0], key=lambda data: data[1], reverse=True)
+clients_pbans = sorted([(client, client.pbans) for client in cl_by_id.values() if client.pbans > 0], key=lambda data: data[1], reverse=True)
+objs = [('Onlinetime', clients_onlinetime), ('Kicks', clients_kicks),
+ ('passive Kicks', clients_pkicks),
+ ('Bans', clients_bans), ('passive Bans', clients_pbans)] # (headline, list)
-# helper functions
-def desc(key, data_dict=clients):
- r = []
- values = {}
- for clid in data_dict:
- if key in data_dict[clid]:
- values[clid] = data_dict[clid][key]
- for clid in sorted(values, key=values.get, reverse=True):
- value = values[clid]
- r.append((clid, data_dict[clid]['nick'], value))
- return r
-
-
-def render_template():
- env = Environment(loader=FileSystemLoader(path))
- template = env.get_template('template.html')
- # format onlinetime
- onlinetime_desc = desc('onlinetime')
- for idx, (clid, nick, onlinetime) in enumerate(onlinetime_desc):
- if onlinetime > 60:
- onlinetime_str = str(onlinetime // 60) + 'h'
- m = onlinetime % 60
- if m > 0:
- onlinetime_str += ' ' + str(m) + 'm'
- else:
- onlinetime_str = str(onlinetime) + 'm'
- onlinetime_desc[idx] = (clid, nick, onlinetime_str, clients[clid]['connected'])
-
- kicks_desc = desc('kicks', data_dict=kicks)
- pkicks_desc = desc('pkicks')
- bans_desc = desc('bans')
- pbans_desc = desc('pbans')
- show_kicks = len(kicks_desc) > 0
- show_pkicks = len(pkicks_desc) > 0
- show_bans = len(bans_desc) > 0
- show_pbans = len(pbans_desc) > 0
- with open(output_path, 'w+') as f:
- f.write(template.render(title=title, onlinetime=onlinetime_desc, kicks=kicks_desc, pkicks=pkicks_desc, bans=bans_desc, pbans=pbans_desc, seconds='{}.{}'.format(generation_delta.seconds, generation_delta.microseconds),
- date=generation_end.strftime('%d.%m.%Y %H:%M'),
- show_onlinetime=show_onlinetime,
- show_kicks=show_kicks,
- show_pkicks=show_pkicks,
- show_bans=show_bans,
- show_pbans=show_pbans))
-
-if len(clients) < 1:
- print('Not enough data!')
-else:
- render_template()
+with open(output_path, 'w') as f:
+ f.write(template.render(title=title, objs=objs, generation_time='{}.{}'.format(generation_delta.seconds, generation_delta.microseconds), time=generation_end.strftime('%d.%m.%Y %H:%M')))