#!/bin/env python3 # # simple log parser and status server for the jitsi meet video conference tool # # Reik Kaps 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: try: room_name = member_match.group(1).strip() room_member = member_match.group(3).strip() except: room_name = 'undefined' room_member = 'undefined' 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