266 lines
8.2 KiB
Python
Executable File
266 lines
8.2 KiB
Python
Executable File
#!/bin/env python3
|
|
#
|
|
# simple log parser and status server for the jitsi meet video conference tool
|
|
#
|
|
# Reik Kaps <r31k@k4p5.de> 2020
|
|
|
|
import re
|
|
import sys
|
|
import threading
|
|
from time import sleep
|
|
from os.path import basename
|
|
from datetime import datetime
|
|
from argparse import ArgumentParser
|
|
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
from urllib.parse import urlparse, parse_qs
|
|
|
|
try:
|
|
from sh import tail
|
|
from pybadges import badge
|
|
|
|
except ImportError as e:
|
|
print('[Error] Please install sh via pip! {}'.format(e))
|
|
sys.exit(1)
|
|
|
|
|
|
# active room dictionary
|
|
roomList = {}
|
|
|
|
# @see https://regex101.com/r/R1B1OL/1
|
|
regex = r"(Jicofo)\s(\d+-\d+-\d+)\s(\d+\:\d+\:\d+\.\d+)\s(\w+):\s\[(\d+)\]\s(.*)\(\)\s(.*)"
|
|
prog = re.compile(regex)
|
|
|
|
##
|
|
# new jitsi room
|
|
room_regex = r"(^Joining\sthe\sroom):(.*)"
|
|
room = re.compile(room_regex)
|
|
|
|
# disposed room
|
|
disposed_regex = r"^Disposed\sconference\sfor\sroom\:\s(.*)@(.*)\sconference\scount:\s(\d+)"
|
|
disposed = re.compile(disposed_regex)
|
|
|
|
##
|
|
# get the room owner
|
|
owner_regex = r"Granted\sowner\sto\s([a-z0-9-]+)\@(.*)\/(.*)"
|
|
owner = re.compile(owner_regex)
|
|
|
|
# get member for room
|
|
member_regex = r"^Member\s(.*)@(.*)\/(.*)\s(joined)."
|
|
member = re.compile(member_regex)
|
|
|
|
# remove/leaving member
|
|
leaving_regex = r"^Member\s(.*)@(.*)\/(.*)\sis\s(leaving)"
|
|
leaving = re.compile(leaving_regex)
|
|
|
|
|
|
class MeetingRoom():
|
|
"""
|
|
represent a Jitsi Meetingroom
|
|
jid: eg room@conference.meet.leinelab.org
|
|
ctime: eg 2020-06-01 12:32:02 ..
|
|
"""
|
|
|
|
def __init__(self, jid, ctime):
|
|
self.jid = jid
|
|
self.ctime = ctime
|
|
self.members = []
|
|
|
|
def setOwner(self, name):
|
|
self.owner = name
|
|
|
|
def getOwner(self):
|
|
if hasattr(self, 'owner'):
|
|
return self.owner
|
|
else:
|
|
return None
|
|
|
|
def addMember(self, name):
|
|
self.members.append(name)
|
|
|
|
def delMember(self, name):
|
|
try:
|
|
self.members.remove(name)
|
|
except ValueError as e:
|
|
pass
|
|
|
|
def getMembers(self):
|
|
return self.members
|
|
|
|
def __str__(self):
|
|
return '{}: {}'.format(self.jid, self.owner)
|
|
|
|
|
|
class logThread(threading.Thread):
|
|
def __init__(self, threadID, name, filename):
|
|
threading.Thread.__init__(self)
|
|
self.threadID = threadID
|
|
self.name = name
|
|
self.filename = filename
|
|
|
|
def run(self):
|
|
while True:
|
|
getLogMsg(self.filename)
|
|
|
|
|
|
class MyServer(BaseHTTPRequestHandler):
|
|
def badge_respones(self, response_string):
|
|
self.send_response(200)
|
|
self.send_header("Content-type", "image/svg+xml")
|
|
self.end_headers()
|
|
self.wfile.write(response_string)
|
|
|
|
def text_response(self, response_string):
|
|
self.send_response(200)
|
|
self.send_header("Content-type", "text/plain")
|
|
self.end_headers()
|
|
self.wfile.write(response_string)
|
|
|
|
def do_GET(self):
|
|
query_components = parse_qs(urlparse(self.path).query)
|
|
if "format" in query_components:
|
|
respones_format = query_components["format"][0]
|
|
else:
|
|
respones_format = 'text'
|
|
|
|
room_name = basename(self.path.split('?')[0])
|
|
print('{} - {}'.format(query_components, room_name))
|
|
if room_name in roomList:
|
|
if respones_format == 'json':
|
|
pass
|
|
elif respones_format == 'svg':
|
|
s = badge(left_text='{}: {}'.format('Room', room_name),
|
|
right_text=str(len(roomList[room_name].members)),
|
|
left_color='blue',
|
|
right_color='green')
|
|
response_string = bytes(s, "utf-8")
|
|
self.badge_respones(response_string)
|
|
|
|
else:
|
|
# default text response
|
|
response_string = bytes(room_name
|
|
+ '|'
|
|
+ str(roomList[room_name].ctime)
|
|
+ '|'
|
|
+ str(len(roomList[room_name].members))
|
|
+ '|', "utf-8")
|
|
self.text_response(response_string)
|
|
else:
|
|
if respones_format == 'svg':
|
|
s = badge(left_text='{}: {}'.format('Room', room_name),
|
|
right_text='empty',
|
|
left_color='red',
|
|
right_color='darkgray')
|
|
response_string = bytes(s, "utf-8")
|
|
self.badge_respones(response_string)
|
|
else:
|
|
self.send_response(404)
|
|
# self.send_header("Content-type", "text/plain")
|
|
self.end_headers()
|
|
|
|
|
|
def getRoomName(jid):
|
|
return jid.split("@")[0].strip()
|
|
|
|
|
|
def mkDatetime(str_date, str_time):
|
|
return datetime.fromisoformat('{} {}'.format(str_date, str_time))
|
|
|
|
|
|
def process_line(line):
|
|
"""
|
|
process a logfile line, using global object and vars
|
|
globals: prog, disposed, owner, member, roomList
|
|
line: string
|
|
"""
|
|
erg = prog.match(line)
|
|
if erg:
|
|
# detect room creation
|
|
new_room = room.match(erg.group(7))
|
|
if new_room:
|
|
room_name = getRoomName(new_room.group(2))
|
|
room_date = mkDatetime(erg.group(2), erg.group(3))
|
|
print('{} {} created'.format(room_date, room_name))
|
|
currentRoom = MeetingRoom(room_name, room_date)
|
|
if room_name in roomList:
|
|
# delete old room and recreate it
|
|
del roomList[room_name]
|
|
roomList[room_name] = currentRoom
|
|
else:
|
|
roomList[room_name] = currentRoom
|
|
|
|
disposed_room = disposed.match(erg.group(7))
|
|
if disposed_room:
|
|
room_name = disposed_room.group(1)
|
|
print('-> room disposed: {}'.format(room_name))
|
|
if room_name in roomList:
|
|
del roomList[room_name]
|
|
|
|
owner_match = owner.match(erg.group(7))
|
|
if owner_match:
|
|
room_name = owner_match.group(1).strip()
|
|
room_owner = owner_match.group(3).strip()
|
|
if room_name in roomList:
|
|
roomList[room_name].setOwner(room_owner)
|
|
|
|
member_match = member.match(erg.group(7))
|
|
if member_match:
|
|
room_name = member_match.group(1).strip()
|
|
room_member = member_match.group(3).strip()
|
|
print('{} enters {}'.format(room_member, room_name))
|
|
if room_name in roomList:
|
|
roomList[room_name].addMember(room_member)
|
|
|
|
# participant is leaving a the room
|
|
leaving_match = leaving.match(erg.group(7))
|
|
if leaving_match:
|
|
room_name = member_match.group(1).strip()
|
|
room_member = member_match.group(3).strip()
|
|
print('{} leaves {}'.format(room_member, room_name))
|
|
if room_name in roomList:
|
|
roomList[room_name].delMember(room_member)
|
|
|
|
|
|
def getLogMsg(logfile):
|
|
"""
|
|
get log messages and commit it to process_line
|
|
used pytail module
|
|
"""
|
|
# for line in Pygtail(logfile, full_lines=True):
|
|
# process_line(line)
|
|
|
|
for line in tail("-f", logfile, _iter=True):
|
|
process_line(line)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
# start cmdline processing
|
|
parser = ArgumentParser(description='Jitsi Meet Conference Stats')
|
|
parser.add_argument('--log', '-l',
|
|
help='full path to jicofo logfile eg.' +
|
|
' /var/log/jitsi/jicofo.log')
|
|
parser.add_argument('--verbose', '-v',
|
|
help='be verbose eg. for debugging',
|
|
action='store_true')
|
|
parser.add_argument('--port', '-p',
|
|
help='set the http server port',
|
|
default=9999)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.log:
|
|
try:
|
|
loggerThread = logThread(1, "Thread-Logger", args.log)
|
|
loggerThread.start()
|
|
except Exception as e:
|
|
print('Error: unable to start threads {}'.format(e))
|
|
|
|
hostName = 'localhost'
|
|
serverPort = args.port
|
|
webServer = HTTPServer((hostName, serverPort), MyServer)
|
|
|
|
try:
|
|
print("Server started http://%s:%s" % (hostName, serverPort))
|
|
webServer.serve_forever()
|
|
except:
|
|
pass
|