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