From 2a36e12da624ff47f44be779972ff889f21a0354 Mon Sep 17 00:00:00 2001 From: "lxbpxylps@126.com" Date: Mon, 4 Oct 2021 22:25:02 +0800 Subject: [PATCH] Upload code --- prism.py | 10 ++ prism/__init__.py | 77 +++++++++++++ prism/api/__init__.py | 230 ++++++++++++++++++++++++++++++++++++++ prism/event/__init__.py | 122 ++++++++++++++++++++ prism/shell/__init__.py | 133 ++++++++++++++++++++++ prism/webhook/__init__.py | 56 ++++++++++ prism_config.json | 26 +++++ 7 files changed, 654 insertions(+) create mode 100644 prism.py create mode 100644 prism/__init__.py create mode 100644 prism/api/__init__.py create mode 100644 prism/event/__init__.py create mode 100644 prism/shell/__init__.py create mode 100644 prism/webhook/__init__.py create mode 100644 prism_config.json diff --git a/prism.py b/prism.py new file mode 100644 index 0000000..6fdb3c8 --- /dev/null +++ b/prism.py @@ -0,0 +1,10 @@ +import json + +from prism import Prism + + +if __name__ == '__main__': + config = json.load(open('./prism_config.json', 'r', encoding='utf-8')) + + prism = Prism(config) + prism.run() diff --git a/prism/__init__.py b/prism/__init__.py new file mode 100644 index 0000000..6f322db --- /dev/null +++ b/prism/__init__.py @@ -0,0 +1,77 @@ +import sys +import asyncio + +from loguru import logger + +from .shell import Shell +from .api import API +from .event import Event +from .webhook import Webhook + + +class Prism: + + def __init__(self, config: dict) -> None: + logger.remove() + + if config['level'] == 'DEBUG': + logger.add( + sys.stdout, + level="DEBUG", + format="{time:HH:mm:ss} | {level: <9} | {message}" + ) + logger.add( + "prism.log", + level="DEBUG", + format="{time:HH:mm:ss} | {level: <9} | {message}", + rotation="10 MB" + ) + else: + logger.add( + sys.stdout, + level="INFO", + format="{time:HH:mm:ss} | {level: <9} | {message}" + ) + logger.add( + "prism.log", + level="INFO", + format="{time:HH:mm:ss} | {level: <9} | {message}", + rotation="10 MB" + ) + + logger.level('DEBUG', color='') + logger.level('INFO', color='') + + logger.debug('Log level set to DEBUG.') + logger.debug('Config:') + logger.debug(config) + + self.shell = Shell(config) + self.api = API(config) + self.event = Event(config) + self.webhook = Webhook(config) + + self.shell.add_line_handler(self.event.line_to_event) + self.api.bind_shell(self.shell) + self.webhook.bind_event(self.event) + + def run(self) -> None: + ''' + Run Prism console. + ''' + + logger.info('Welcome to Prism console.\n') + + loop = asyncio.get_event_loop() + + tasks = asyncio.gather( + self.shell.get_tasks(), + self.api.get_tasks(), + self.webhook.get_tasks() + ) + + try: + loop.run_until_complete(tasks) + except KeyboardInterrupt: + self.shell.kill_game() + logger.info('Keyboard interrupt. Goodbye.') diff --git a/prism/api/__init__.py b/prism/api/__init__.py new file mode 100644 index 0000000..1584119 --- /dev/null +++ b/prism/api/__init__.py @@ -0,0 +1,230 @@ +import asyncio +import re +import json + +from loguru import logger +from aiohttp import web + +from prism.shell import Shell + + +class API: + + def __init__(self, config: dict) -> None: + logger.level('API', no=20, color='') + + self.api_address: str = config['api']['address'] + self.api_port: int = config['api']['port'] + self.api_tag: str = config['api']['tag'] + + self.shell = None + + def bind_shell(self, shell: Shell) -> None: + self.shell = shell + + def get_tasks(self): + return asyncio.gather( + self._start_api() + ) + + async def _start_api(self) -> None: + app = web.Application() + app.add_routes( + [ + web.get('/', self._api_get_root_handler), + web.post('/cmd', self._api_post_cmd_handler), + web.get('/list', self._api_get_list_handler), + web.post('/tellraw', self._api_post_tellraw_handler), + web.get('/usercache', self._api_get_usercache_handler) + ] + ) + + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, self.api_address, self.api_port) + await site.start() + + logger.log( + 'API', + f'Start API Server on http://{self.api_address}:{self.api_port}' + ) + + async def _api_get_root_handler(self, request: web.Request) -> web.Response: + logger.log('API', 'GET: /') + + data = { + "status": 200, + "msg": "success", + "data": { + "tag": self.api_tag + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) + + async def _api_post_cmd_handler(self, request: web.Request) -> web.Response: + req = await request.json() + + logger.log('API', 'POST: /cmd ' + str(dict(req))) + + if 'cmd' in req.keys(): + cmd = str(req['cmd']) + else: + data = { + "status": 400, + "msg": "no cmd", + "data": { + "tag": self.api_tag + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) + + if 'num' in req.keys(): + num = int(req['num']) + + if num < 1: + num = 1 + else: + num = 1 + + if 'wait_time' in req.keys(): + wait_time = int(req['wait_time']) + + if wait_time < 1: + wait_time = 0 + else: + wait_time = 1 + + if self.shell.run_flag.is_set(): + async with self.shell.line_queue_get_lock: + self.shell.send_game_cmd(cmd) + await asyncio.sleep(wait_time) + ret_list = await self.shell.temp_get_lines(num) + + data = { + "status": 200, + "msg": "success", + "data": { + "tag": self.api_tag, + "list": ret_list + } + } + else: + data = { + "status": 406, + "msg": "game stop", + "data": { + "tag": self.api_tag, + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) + + async def _api_get_list_handler(self, request: web.Request) -> web.Response: + logger.log('API', 'GET: /list') + + if self.shell.run_flag.is_set(): + async with self.shell.line_queue_get_lock: + self.shell.send_game_cmd('list') + await asyncio.sleep(1) + ret_list = await self.shell.temp_get_lines(1) + + player_str: str = re.findall("online:(.*?)$", ret_list[0])[0] + player_str = player_str.strip() + + if player_str == '': + player_list = [] + else: + player_list = player_str.split(', ') + + data = { + "status": 200, + "msg": "success", + "data": { + "tag": self.api_tag, + "num": len(player_list), + "player_list": player_list + } + } + else: + data = { + "status": 406, + "msg": "game stop", + "data": { + "tag": self.api_tag, + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) + + async def _api_post_tellraw_handler(self, request: web.Request) -> web.Response: + req = await request.json() + + logger.log('API', 'POST: /tellraw ' + str(dict(req))) + + if 'message' in req.keys(): + message = str(req['message']) + else: + data = { + "status": 400, + "msg": "no message", + "data": { + "tag": self.api_tag + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) + + if 'selector' in req.keys(): + selector = str(req['selector']) + else: + selector = '@a' + + if self.shell.run_flag.is_set(): + cmd = 'tellraw ' + selector + ' {"text": "' + message + '\"}' + cmd = cmd.replace('\n', '\\n') + logger.debug(cmd) + self.shell.send_game_cmd( + cmd + ) + + data = { + "status": 200, + "msg": "success", + "data": { + "tag": self.api_tag + } + } + else: + data = { + "status": 406, + "msg": "game stop", + "data": { + "tag": self.api_tag, + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) + + async def _api_get_usercache_handler(self, request: web.Request) -> web.Response: + logger.log('API', 'GET: /usercache') + user_cache_list = json.load(open('./usercache.json', 'r')) + + data = { + "status": 200, + "msg": "success", + "data": { + "tag": self.api_tag, + "usercache": user_cache_list + } + } + + logger.log('API', f'RETURN: {data}') + return web.json_response(data) diff --git a/prism/event/__init__.py b/prism/event/__init__.py new file mode 100644 index 0000000..2354b13 --- /dev/null +++ b/prism/event/__init__.py @@ -0,0 +1,122 @@ +import time +import asyncio +import re + +from loguru import logger + + +class BaseEvent: + + def to_dict(self) -> dict: + return vars(self) + + +class ServerStartEvent(BaseEvent): + + def __init__(self) -> None: + self.time = int(time.time()) + self.type: str = 'ServerStart' + self.start_use_time: float + + +class ServerStopEvent(BaseEvent): + + def __init__(self) -> None: + self.time = int(time.time()) + self.type: str = 'ServerStop' + + +class PlayerJoinEvent(BaseEvent): + + def __init__(self) -> None: + self.time = int(time.time()) + self.type: str = 'PlayerJoin' + self.player: str + + +class PlayerQuitEvent(BaseEvent): + + def __init__(self) -> None: + self.time = int(time.time()) + self.type: str = 'PlayerQuit' + self.player: str + + +class PlayerChatEvent(BaseEvent): + + def __init__(self) -> None: + self.time = int(time.time()) + self.type: str = 'PlayerChat' + self.player: str + self.message: str + + +class PlayerAdvancementEvent(BaseEvent): + + def __init__(self) -> None: + self.time = int(time.time()) + self.type: str = 'PlayerAdvancement' + self.player: str + self.advancement: str + + +class Event: + + def __init__(self, config: dict) -> None: + logger.level('EVENT', no=20, color='') + + self.event_queue = asyncio.Queue() + + def line_to_event(self, line: str) -> None: + res = re.findall( + "]: Done \((.*?)s\)! For help, type \"help\"", line) + if res: + event = ServerStartEvent() + event.start_use_time = float(res[0]) + self._save_event(event) + return + + if line.find('PRISM CLOSE SINGAL') != -1: + event = ServerStopEvent() + self._save_event(event) + return + + res = re.findall("]: (.*?) joined the game", line) + if res: + event = PlayerJoinEvent() + event.player = res[0] + self._save_event(event) + return + + res = re.findall("]: (.*?) left the game", line) + if res: + event = PlayerQuitEvent() + event.player = res[0] + self._save_event(event) + return + + res = re.findall("]: <(.*?)> ", line) + if res: + event = PlayerChatEvent() + event.player = res[0] + res = re.findall("<[\s\S]*> (.*?)$", line) + event.message = res[0] + self._save_event(event) + return + + res = re.findall("]: (.*?) has[\s\S]*\[[\s\S]*]", line) + if res: + event = PlayerAdvancementEvent() + event.player = res[0] + res = re.findall("has[\s\S]*\[(.*?)]", line) + event.advancement = res[0] + self._save_event(event) + return + + def _save_event(self, event) -> None: + logger.log('EVENT', f'Event: {event.to_dict()}') + self.event_queue.put_nowait(event) + + async def get_event(self): + res = await self.event_queue.get() + return res diff --git a/prism/shell/__init__.py b/prism/shell/__init__.py new file mode 100644 index 0000000..a7b364c --- /dev/null +++ b/prism/shell/__init__.py @@ -0,0 +1,133 @@ +import asyncio + +from loguru import logger +import aioconsole + + +class Shell: + + def __init__(self, config: dict) -> None: + logger.level('MINECRAFT', no=20, color='') + + self.shell_start_cmd: str = config['shell']['start_cmd'] + self.shell_stop_cmd: str = config['shell']['stop_cmd'] + self.shell_read_encoding: str = config['shell']['read_encoding'] + self.shell_write_encoding: str = config['shell']['write_encoding'] + + self.proc = None + self.run_flag: asyncio.Event = asyncio.Event() + self.run_flag.clear() + + self.line_queue = asyncio.Queue() + self.line_queue_get_lock = asyncio.Lock() + + self.line_handler: list = [] + + self.add_line_handler(self._game_stop_handler) + + def get_tasks(self): + return asyncio.gather( + self.start_game(), + self._receiver(), + self._console_input(), + self._call_line_handler() + ) + + def add_line_handler(self, func) -> None: + self.line_handler.append(func) + + async def start_game(self) -> None: + self.proc = await asyncio.create_subprocess_shell( + self.shell_start_cmd + " && echo PRISM CLOSE SINGAL", + stdout=asyncio.subprocess.PIPE, + stdin=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + + self.run_flag.set() + + logger.info('Game Start..') + + def stop_game(self) -> None: + self.send_game_cmd('stop') + + def kill_game(self) -> None: + try: + self.proc.kill() + except ProcessLookupError: + pass + + async def _receiver(self) -> None: + while True: + await self.run_flag.wait() + + data = await self.proc.stdout.readline() + line = data.decode(self.shell_read_encoding).rstrip() + + # Display the line. + if line.strip() != '': + logger.log('MINECRAFT', line) + + # Put the line into queue. + await self.line_queue.put(line) + + def send_game_cmd(self, cmd: str) -> None: + if self.run_flag.is_set(): + self.proc.stdin.write( + (cmd + '\n').encode(self.shell_write_encoding)) + + async def _console_input(self) -> None: + while True: + cmd = await aioconsole.ainput() + + if self.run_flag.is_set(): + self.send_game_cmd(cmd) + else: + await self.issue_prism_cmd(cmd) + + async def _call_line_handler(self) -> None: + while True: + await self.run_flag.wait() + await asyncio.sleep(0) + + async with self.line_queue_get_lock: + try: + line = self.line_queue.get_nowait() + except asyncio.QueueEmpty: + continue + + for func in self.line_handler: + func(line) + + async def temp_get_lines(self, num: int = 1) -> list: + ''' + Get and put back lines to the line_queue. + Before call this method, make sure the line_queue's get method is locked. + + Example: + + async with handler.queue_get_lock: + ret_list = await handler.temp_get_lines() + + ''' + + line_list = [] + + for i in range(num): + try: + line_list.append(self.line_queue.get_nowait()) + except asyncio.QueueEmpty: + pass + + # Put back to queue. + for line in line_list: + await self.line_queue.put(line) + + return line_list + + def _game_stop_handler(self, line: str): + if line.find('PRISM CLOSE SINGAL') != -1: + self.kill_game() + self.run_flag.clear() + + logger.info('Server closed. Type start to start the server.') diff --git a/prism/webhook/__init__.py b/prism/webhook/__init__.py new file mode 100644 index 0000000..80d3c5b --- /dev/null +++ b/prism/webhook/__init__.py @@ -0,0 +1,56 @@ +import asyncio + +from loguru import logger +import aiohttp + +from prism.event import Event + + +class Webhook: + + def __init__(self, config: dict) -> None: + logger.level('WEBHOOK', no=20, color='') + + self.webhook_url: str = config['webhook']['url'] + self.webhook_tag: str = config['webhook']['tag'] + self.webhook_event: dict = config['webhook']['event'] + self.allow_event_list = [] + + for key in self.webhook_event: + if self.webhook_event[key] == True: + self.allow_event_list.append(key) + + self.event = None + + def bind_event(self, event: Event) -> None: + self.event = event + + def get_tasks(self): + return asyncio.gather( + self._handle_event() + ) + + async def _handle_event(self) -> None: + while True: + event_obj = await self.event.get_event() + + data = { + "status": 200, + "msg": "event", + "data": { + "tag": self.webhook_tag + } + } + + data['data'].update(event_obj.to_dict()) + + if data['data']['type'] not in self.allow_event_list: + logger.log('WEBHOOK', f'Webhook for this event is disabled.') + return + + try: + async with aiohttp.ClientSession() as session: + async with session.post(url=self.webhook_url, json=data) as resp: + logger.log('WEBHOOK', f'Send: {data}') + except (ConnectionRefusedError, aiohttp.ClientConnectionError) as e: + logger.log('WEBHOOK', f'Error: {type(e)}') diff --git a/prism_config.json b/prism_config.json new file mode 100644 index 0000000..9740e2d --- /dev/null +++ b/prism_config.json @@ -0,0 +1,26 @@ +{ + "level": "DEBUG", + "shell": { + "start_cmd": "./start.sh", + "stop_cmd": "stop", + "read_encoding": "utf-8", + "write_encoding": "utf-8" + }, + "api": { + "address": "127.0.0.1", + "port": 8520, + "tag": "rpg_server" + }, + "webhook": { + "url": "http://example.com:8080/", + "tag": "rpg_server", + "event": { + "ServerStart": true, + "ServerStop": true, + "PlayerJoin": true, + "PlayerQuit": true, + "PlayerChat": true, + "PlayerAdvancement": true + } + } +} \ No newline at end of file