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 logging
import argparse
import inspect
from pathlib import Path
from importlib import import_module
from typing import get_type_hints
from dataclasses import dataclass
from dataclasses import dataclass, MISSING
from setproctitle import setproctitle
@ -19,70 +20,28 @@ def main():
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
# addon_path = Path(args.path) if args.addon_path else ( root/'addons' )
addon_path = root/'addons'
addon_path = Path('addons')
addons : List[Type[Addon]] = []
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():
obj = getattr(m, obj_name)
if issubclass(obj, Addon):
if obj != Addon and inspect.isclass(obj) and issubclass(obj, Addon):
addons.append(obj)
class ChatLogger(Addon):
"""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'
help_text = '\n\naddons:'
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@self.on_packet(PacketChatMessage)
async def print_chat_colored_to_terminal(packet:PacketChatMessage):
print(self.REMOVE_COLOR_FORMATS.sub("", parse_chat(packet.message, ansi_color=self.config.color)))
addons.append(ChatLogger)
class ChatInput(Addon):
"""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
)
)
for addon in addons:
help_text += f"\n {addon.__name__} \t{addon.__doc__ or ''}"
cfg_clazz = get_type_hints(addon)['config']
for name, field in cfg_clazz.__dataclass_fields__.items():
default = field.default if field.default is not MISSING \
else field.default_factory() if field.default_factory is not MISSING \
else MISSING
help_text += f"\n * {name} ({field.type.__name__}) | {'-required-' if default is MISSING else f'{default}'}"
help_text += '\n'
parser = argparse.ArgumentParser(
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('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('--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')
@ -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-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('--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()
configure_logging(args.name, level=logging.DEBUG if args._debug else logging.INFO)
setproctitle(f"treepuncher[{args.name}]")
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(
args.name,
args.server,
use_packet_whitelist=use_packet_whitelist,
notifier=notifier,
login_code=code,
client_id=args.cid,
client_secret=args.secret,
redirect_uri=args.uri
@ -122,13 +84,6 @@ def main():
for addon in addons:
client.install(addon)
if args.chat_log:
client.install(ChatLogger)
if args.chat_input:
from aioconsole import ainput
client.install(ChatInput)
client.run()
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")
print_formatter : logging.Formatter
if color:
print_formatter = ColorFormatter("├🮥 %(message)s")
print_formatter = ColorFormatter("> %(message)s")
else:
print_formatter = logging.Formatter("├🮥 %(message)s")
print_formatter = logging.Formatter("> %(message)s")
fh.setFormatter(file_formatter)
ch.setFormatter(print_formatter)
# 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))
self.db.commit()
def system(self) -> SystemState:
def system(self) -> Optional[SystemState]:
cur = self.db.cursor()
val = cur.execute('SELECT * FROM system').fetchall()
if not val:
raise ValueError("No system state set")
return None
return SystemState(
name=val[0][0],
token=val[0][1],

View file

@ -1,4 +1,5 @@
import re
import json
import logging
import asyncio
import datetime
@ -14,7 +15,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from aiocraft.client import MinecraftClient
from aiocraft.mc.packet import Packet
from .storage import Storage
from .storage import Storage, SystemState
from .notifier import Notifier
from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld
@ -26,34 +27,38 @@ class ConfigObject:
class Addon:
name : str
config : ConfigObject
_client : 'Treepuncher'
@dataclass(frozen=True)
class Options(ConfigObject):
pass
config : Options
@property
def client(self) -> 'Treepuncher':
return self._client
def __init__(self, client:'Treepuncher'):
def __init__(self, client:'Treepuncher', *args, **kwargs):
self._client = client
self.name = type(self).__name__
cfg = self._client.config
kwargs : Dict[str, Any] = {}
opts : Dict[str, Any] = {}
for name, clazz in get_type_hints(self.Options).items():
default = getattr(self.Options, name, None)
if cfg.has_option(self.name, name):
if clazz is bool:
kwargs[name] = self._client.config[self.name].getboolean(name)
opts[name] = self._client.config[self.name].getboolean(name)
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:
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
kwargs[name] = default
self.config = self.Options(**kwargs)
opts[name] = default
self.config = self.Options(**opts)
self.register(client)
def register(self, client:'Treepuncher'):
pass
async def initialize(self):
pass
@ -87,6 +92,11 @@ class Treepuncher(
self.config.read(config_path)
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 = []
@ -107,6 +117,15 @@ class Treepuncher(
return self._username
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):
await self.notifier.initialize(self)
for m in self.modules: