improvements, tweaks to addons system

This commit is contained in:
əlemi 2022-02-16 02:46:47 +01:00
parent 78b97a42a6
commit d5934da832
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
5 changed files with 54 additions and 80 deletions

View file

@ -1 +1 @@
from .treepuncher import Treepuncher from .treepuncher import Treepuncher, Addon, ConfigObject

View file

@ -4,11 +4,12 @@ import re
import asyncio import asyncio
import logging import logging
import argparse import argparse
import inspect
from pathlib import Path from pathlib import Path
from importlib import import_module from importlib import import_module
from typing import get_type_hints from typing import get_type_hints
from dataclasses import dataclass from dataclasses import dataclass, MISSING
from setproctitle import setproctitle from setproctitle import setproctitle
@ -19,70 +20,28 @@ def main():
root = Path(os.getcwd()) root = Path(os.getcwd())
# TODO would be cool if it was possible to configure addons path, but we need to load addons before doing argparse so we can do helptext # TODO would be cool if it was possible to configure addons path, but we need to load addons before doing argparse so we can do helptext
# addon_path = Path(args.path) if args.addon_path else ( root/'addons' ) # addon_path = Path(args.path) if args.addon_path else ( root/'addons' )
addon_path = root/'addons' addon_path = Path('addons')
addons : List[Type[Addon]] = [] addons : List[Type[Addon]] = []
for path in sorted(addon_path.rglob('*.py')): for path in sorted(addon_path.rglob('*.py')):
m = import_module(path) py_path = str(path).replace('/', '.').replace('.py', '')
m = import_module(py_path)
for obj_name in vars(m).keys(): for obj_name in vars(m).keys():
obj = getattr(m, obj_name) obj = getattr(m, obj_name)
if issubclass(obj, Addon): if obj != Addon and inspect.isclass(obj) and issubclass(obj, Addon):
addons.append(obj) addons.append(obj)
class ChatLogger(Addon): help_text = '\n\naddons:'
"""print (optionally colored) game chat to terminal"""
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
@dataclass
class Options(ConfigObject):
test : str
something : int
color : bool = True
blah : str = 'porcodio'
def __init__(self, *args, **kwargs): for addon in addons:
super().__init__(*args, **kwargs) help_text += f"\n {addon.__name__} \t{addon.__doc__ or ''}"
@self.on_packet(PacketChatMessage) cfg_clazz = get_type_hints(addon)['config']
async def print_chat_colored_to_terminal(packet:PacketChatMessage): for name, field in cfg_clazz.__dataclass_fields__.items():
print(self.REMOVE_COLOR_FORMATS.sub("", parse_chat(packet.message, ansi_color=self.config.color))) default = field.default if field.default is not MISSING \
else field.default_factory() if field.default_factory is not MISSING \
addons.append(ChatLogger) else MISSING
help_text += f"\n * {name} ({field.type.__name__}) | {'-required-' if default is MISSING else f'{default}'}"
class ChatInput(Addon): help_text += '\n'
"""read input from stdin and send to game chat"""
task : asyncio.Task
running : bool
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.task = None
self.running = False
async def initialize(self):
async def aio_input():
while self.running:
try:
await self.client.chat(await asyncio.wait_for(ainput(""), 1))
except asyncio.TimeoutError:
pass
except Exception:
client._logger.exception("Exception processing input from keyboard")
self.running = True
self.task = asyncio.get_event_loop().create_task(aio_input())
async def cleanup(self, force:bool=False):
self.running = False
if self.task and not force:
await self.task
addons.append(ChatInput)
help_text = '\n\naddons:\n' + str.join( # TODO do this iteratively to make it readable!
'\n', (
f" {addon.__name__}\t\t{addon.__doc__ or '-no description-'}\n " + str.join('\n ',
(f"* {name} ({clazz.__name__}) {'[required]' if not hasattr(addon.Options, name) else ''}" for (name, clazz) in get_type_hints(addon.Options).items())
) for addon in addons
)
)
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog='python -m treepuncher', prog='python -m treepuncher',
@ -93,6 +52,7 @@ def main():
parser.add_argument('name', help='name to use for this client session') parser.add_argument('name', help='name to use for this client session')
parser.add_argument('server', help='server to connect to') parser.add_argument('server', help='server to connect to')
parser.add_argument('-c', '--code', dest='code', action='store_const', const=True, default=False, help="request new code to login")
parser.add_argument('--client-id', dest='cid', default='c63ef189-23cb-453b-8060-13800b85d2dc', help='client_id of your Azure application') parser.add_argument('--client-id', dest='cid', default='c63ef189-23cb-453b-8060-13800b85d2dc', help='client_id of your Azure application')
parser.add_argument('--secret', dest='secret', default='N2e7Q~ybYA0IO39KB1mFD4GmoYzISRaRNyi59', help='client_secret of your Azure application') parser.add_argument('--secret', dest='secret', default='N2e7Q~ybYA0IO39KB1mFD4GmoYzISRaRNyi59', help='client_secret of your Azure application')
parser.add_argument('--redirect-uri', dest='uri', default='https://fantabos.co/msauth', help='redirect_uri of your Azure application') parser.add_argument('--redirect-uri', dest='uri', default='https://fantabos.co/msauth', help='redirect_uri of your Azure application')
@ -100,20 +60,22 @@ def main():
parser.add_argument('--chat-log', dest='chat_log', action='store_const', const=True, default=False, help="print (colored) chat to terminal") parser.add_argument('--chat-log', dest='chat_log', action='store_const', const=True, default=False, help="print (colored) chat to terminal")
parser.add_argument('--chat-input', dest='chat_input', action='store_const', const=True, default=False, help="read input from stdin and send it to chat") parser.add_argument('--chat-input', dest='chat_input', action='store_const', const=True, default=False, help="read input from stdin and send it to chat")
parser.add_argument('--debug', dest='_debug', action='store_const', const=True, default=False, help="enable debug logs") parser.add_argument('--debug', dest='_debug', action='store_const', const=True, default=False, help="enable debug logs")
parser.add_argument('--no-packet-whitelist', dest='use_packet_whitelist', action='store_const', const=False, default=True, help="disable packet whitelist") parser.add_argument('--no-packet-filter', dest='use_packet_whitelist', action='store_const', const=False, default=True, help="disable packet whitelist, will decrease performance")
args = parser.parse_args() args = parser.parse_args()
configure_logging(args.name, level=logging.DEBUG if args._debug else logging.INFO) configure_logging(args.name, level=logging.DEBUG if args._debug else logging.INFO)
setproctitle(f"treepuncher[{args.name}]") setproctitle(f"treepuncher[{args.name}]")
code = input(f"-> Go to 'https://fantabos.co/msauth?client_id={args.cid}&state=hardcoded', click 'Auth' and login, then copy here the code you received\n--> ") code = None
if args.code:
code = input(f"-> Go to 'https://fantabos.co/msauth?client_id={args.cid}&state=hardcoded', click 'Auth' and login, then copy here the code you received\n--> ")
client = Treepuncher( client = Treepuncher(
args.name, args.name,
args.server, args.server,
use_packet_whitelist=use_packet_whitelist, use_packet_whitelist=use_packet_whitelist,
notifier=notifier, login_code=code,
client_id=args.cid, client_id=args.cid,
client_secret=args.secret, client_secret=args.secret,
redirect_uri=args.uri redirect_uri=args.uri
@ -122,13 +84,6 @@ def main():
for addon in addons: for addon in addons:
client.install(addon) client.install(addon)
if args.chat_log:
client.install(ChatLogger)
if args.chat_input:
from aioconsole import ainput
client.install(ChatInput)
client.run() client.run()
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -39,9 +39,9 @@ def configure_logging(name:str, level=logging.INFO, color:bool = True):
file_formatter = logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s", "%b %d %Y %H:%M:%S") file_formatter = logging.Formatter("[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s", "%b %d %Y %H:%M:%S")
print_formatter : logging.Formatter print_formatter : logging.Formatter
if color: if color:
print_formatter = ColorFormatter("├🮥 %(message)s") print_formatter = ColorFormatter("> %(message)s")
else: else:
print_formatter = logging.Formatter("├🮥 %(message)s") print_formatter = logging.Formatter("> %(message)s")
fh.setFormatter(file_formatter) fh.setFormatter(file_formatter)
ch.setFormatter(print_formatter) ch.setFormatter(print_formatter)
# add the handlers to the logger # add the handlers to the logger

View file

@ -36,11 +36,11 @@ class Storage:
cur.execute('INSERT INTO system VALUES (?, ?, ?)', (state.name, state.token, state.start_time)) cur.execute('INSERT INTO system VALUES (?, ?, ?)', (state.name, state.token, state.start_time))
self.db.commit() self.db.commit()
def system(self) -> SystemState: def system(self) -> Optional[SystemState]:
cur = self.db.cursor() cur = self.db.cursor()
val = cur.execute('SELECT * FROM system').fetchall() val = cur.execute('SELECT * FROM system').fetchall()
if not val: if not val:
raise ValueError("No system state set") return None
return SystemState( return SystemState(
name=val[0][0], name=val[0][0],
token=val[0][1], token=val[0][1],

View file

@ -1,4 +1,5 @@
import re import re
import json
import logging import logging
import asyncio import asyncio
import datetime import datetime
@ -14,7 +15,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from aiocraft.client import MinecraftClient from aiocraft.client import MinecraftClient
from aiocraft.mc.packet import Packet from aiocraft.mc.packet import Packet
from .storage import Storage from .storage import Storage, SystemState
from .notifier import Notifier from .notifier import Notifier
from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld
@ -26,34 +27,38 @@ class ConfigObject:
class Addon: class Addon:
name : str name : str
config : ConfigObject
_client : 'Treepuncher' _client : 'Treepuncher'
@dataclass(frozen=True) @dataclass(frozen=True)
class Options(ConfigObject): class Options(ConfigObject):
pass pass
config : Options
@property @property
def client(self) -> 'Treepuncher': def client(self) -> 'Treepuncher':
return self._client return self._client
def __init__(self, client:'Treepuncher'): def __init__(self, client:'Treepuncher', *args, **kwargs):
self._client = client self._client = client
self.name = type(self).__name__ self.name = type(self).__name__
cfg = self._client.config cfg = self._client.config
kwargs : Dict[str, Any] = {} opts : Dict[str, Any] = {}
for name, clazz in get_type_hints(self.Options).items(): for name, clazz in get_type_hints(self.Options).items():
default = getattr(self.Options, name, None) default = getattr(self.Options, name, None)
if cfg.has_option(self.name, name): if cfg.has_option(self.name, name):
if clazz is bool: if clazz is bool:
kwargs[name] = self._client.config[self.name].getboolean(name) opts[name] = self._client.config[self.name].getboolean(name)
else: else:
kwargs[name] = clazz(self._client.config[self.name].get(name)) opts[name] = clazz(self._client.config[self.name].get(name))
elif default is None: elif default is None:
raise ValueError(f"Missing required value '{name}' of type '{clazz.__name__}' in section '{self.name}'") raise ValueError(f"Missing required value '{name}' of type '{clazz.__name__}' in section '{self.name}'")
else: # not really necessary since it's a dataclass but whatever else: # not really necessary since it's a dataclass but whatever
kwargs[name] = default opts[name] = default
self.config = self.Options(**kwargs) self.config = self.Options(**opts)
self.register(client)
def register(self, client:'Treepuncher'):
pass
async def initialize(self): async def initialize(self):
pass pass
@ -87,6 +92,11 @@ class Treepuncher(
self.config.read(config_path) self.config.read(config_path)
self.storage = Storage(self.name) self.storage = Storage(self.name)
prev = self.storage.system() # if this isn't 1st time, this won't be None. Load token from there
if prev:
if self.name != prev.name:
self._logger.warning("Saved token session name differs from current")
self._authenticator.deserialize(json.loads(prev.token))
self.modules = [] self.modules = []
@ -107,6 +117,15 @@ class Treepuncher(
return self._username return self._username
raise ValueError("No username configured for offline mode") raise ValueError("No username configured for offline mode")
async def authenticate(self):
await super().authenticate()
state = SystemState(
name=self.name,
token=json.dumps(self._authenticator.serialize()),
start_time=int(time())
)
self.storage._set_state(state)
async def start(self): async def start(self):
await self.notifier.initialize(self) await self.notifier.initialize(self)
for m in self.modules: for m in self.modules: