""" The panopticon provides a way to observe (& control) all running Hugveys trough a web interface """ import logging import tornado import string import random import tornado.websocket import tornado.web import tornado.ioloop import os from pytz.reference import Central import asyncio import json from urllib.parse import urlparse from hugvey import central_command logger = logging.getLogger("panopticon") web_dir = os.path.join(os.path.split(__file__)[0], '..', 'www') def getWebSocketHandler(central_command): class WebSocketHandler(tornado.websocket.WebSocketHandler): CORS_ORIGINS = ['localhost'] connections = set() def check_origin(self, origin): parsed_origin = urlparse(origin) # parsed_origin.netloc.lower() gives localhost:3333 valid = parsed_origin.hostname in self.CORS_ORIGINS return valid # the client connected def open(self): self.connections.add(self) logger.info("New client connected") # the client sent the message def on_message(self, message): logger.debug(f"recieve: {message}") try: msg = json.loads(message) if msg['action'] == 'init': self.msgInit() elif msg['action'] == 'get_status': self.msgStatus() elif msg['action'] == 'resume': self.msgResume(msg['hugvey']) elif msg['action'] == 'pause': self.msgPause(msg['hugvey']) elif msg['action'] == 'restart': self.msgRestart(msg['hugvey']) elif msg['action'] == 'change_language': self.msgChangeLanguage(msg['hugvey'], msg['lang_code']) elif msg['action'] == 'play_msg': self.msgPlayMsg(msg['hugvey'], msg['msg_id']) else: self.send({'alert': 'Unknown request: {}'.format(message)}) logger.warn('Unknown request: {}'.format(message)) except Exception as e: self.send({'alert': 'Invalid request: {}'.format(e)}) logger.exception(e) def send(self, message): j = json.dumps(message) [con.write_message(j) for con in self.connections] # client disconnected def on_close(self): self.connections.remove(self) logger.info("Client disconnected") def getStatusMsg(self): msg = central_command.getStatusSummary() msg['action'] = 'status' return msg def msgStatus(self): self.send(self.getStatusMsg()) def msgInit(self): msg = self.getStatusMsg() self.send(msg) def msgResume(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'resume'}) def msgPause(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'pause'}) def msgRestart(self, hv_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'restart'}) def msgChangeLanguage(self, hv_id, lang_code): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_language', 'lang_code': lang_code}) def msgPlayMsg(self, hv_id, msg_id): central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id}) return WebSocketHandler class NonCachingStaticFileHandler(tornado.web.StaticFileHandler): def set_extra_headers(self, path): # Disable cache self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') def getUploadHandler(central_command): class UploadHandler(tornado.web.RequestHandler): def set_default_headers(self): # headers for CORS self.set_header("Access-Control-Allow-Origin", "*") self.set_header("Access-Control-Allow-Headers", "x-requested-with") self.set_header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS') def options(self): # OPTIONS request for CORS self.set_status(204) self.finish() def post(self): print('upload') langCode = self.get_argument("language") langFile = os.path.join(central_command.config['web']['files_dir'] , central_command.languageFiles[langCode]) storyData = json.loads(self.request.files['json'][0]['body']) # print(json.dumps(storyData)) # self.finish() # return if 'audio' in self.request.files: msgId = self.get_argument("message_id") audioFile = self.request.files['audio'][0] original_fname = audioFile['filename'] fname = ''.join(random.choice(string.ascii_lowercase + string.digits) for x in range(10)) ext = os.path.splitext(original_fname)[1] audioFilename = os.path.join(central_command.config['web']['files_dir'], langCode, fname + ext) for i, data in enumerate(storyData): if data['@id'] != msgId: continue if 'audio' in storyData[i] and os.path.exists(storyData[i]['audio']['file']): logger.info(f"Remove previous file {storyData[i]['audio']['file']} ({storyData[i]['audio']['original_name']})") os.unlink(storyData[i]['audio']['file']) storyData[i]['audio'] = { 'file': audioFilename, 'original_name': original_fname } with open(audioFilename, 'wb') as fp: logger.info(f'Save {original_fname} to {audioFilename}') fp.write(audioFile['body']) break print(os.path.abspath(langFile)) with open(langFile, 'w') as json_fp: logger.info(f'Save story to {langFile} {json_fp}') json.dump(storyData, json_fp) # Reload language files for new instances central_command.loadLanguages() self.finish() return UploadHandler class Panopticon(object): def __init__(self, central_command, config): self.command = central_command self.config = config self.application = tornado.web.Application([ (r"/ws", getWebSocketHandler(self.command)), (r"/local/(.*)", NonCachingStaticFileHandler, {"path": config['web']['files_dir']}), (r"/upload", getUploadHandler(self.command)), (r"/(.*)", tornado.web.StaticFileHandler, {"path": web_dir, "default_filename": 'index.html'}), ], debug=True) self.application.listen(config['web']['port']) # self.loop.configure(evt_loop) def start(self): evt_loop = asyncio.new_event_loop() asyncio.set_event_loop(evt_loop) self.loop = tornado.ioloop.IOLoop.current() logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}") self.loop.start() def stop(self): self.loop.stop()