improvements, tweaks to addons system
This commit is contained in:
parent
78b97a42a6
commit
d5934da832
5 changed files with 54 additions and 80 deletions
|
@ -1 +1 @@
|
|||
from .treepuncher import Treepuncher
|
||||
from .treepuncher import Treepuncher, Addon, ConfigObject
|
||||
|
|
|
@ -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 = 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(
|
||||
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__":
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in a new issue