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 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 = 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--> ")
|
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__":
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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],
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue