Various fixes, implemented credentials storage and addons with config vars

This commit is contained in:
əlemi 2022-02-16 04:07:11 +01:00
parent d5934da832
commit d12b4b4f88
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
5 changed files with 49 additions and 35 deletions

View file

@ -36,6 +36,8 @@ def main():
for addon in addons:
help_text += f"\n {addon.__name__} \t{addon.__doc__ or ''}"
cfg_clazz = get_type_hints(addon)['config']
if cfg_clazz is ConfigObject:
continue # it's the superclass type hint
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 \
@ -61,6 +63,8 @@ def main():
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-filter', dest='use_packet_whitelist', action='store_const', const=False, default=True, help="disable packet whitelist, will decrease performance")
parser.add_argument('--addons', dest='add', nargs='+', type=str, default=[a.__name__ for a in addons], help='specify addons to enable, defaults to all')
# TODO find a better way to specify which addons are enabled
args = parser.parse_args()
@ -74,15 +78,18 @@ def main():
client = Treepuncher(
args.name,
args.server,
use_packet_whitelist=use_packet_whitelist,
use_packet_whitelist=args.use_packet_whitelist,
login_code=code,
client_id=args.cid,
client_secret=args.secret,
redirect_uri=args.uri
)
enabled_addons = set(a.lower() for a in args.add)
for addon in addons:
client.install(addon)
if addon.__name__.lower() in enabled_addons:
logging.info("Installing '%s'", addon.__name__)
client.install(addon)
client.run()

View file

@ -1,5 +1,6 @@
import asyncio
import datetime
import functools
from aiocraft.client import MinecraftClient
from aiocraft.mc.definitions import Gamemode, Dimension, Difficulty
@ -27,20 +28,20 @@ class GameState(MinecraftClient):
def on_death(self):
def decorator(fun):
@functool.wraps(fun)
@functools.wraps(fun)
async def wrapper():
event = DeathEvent()
return await fun(event)
return self.register(DeathEvent.SENTINEL, fun)
return self.register(DeathEvent.SENTINEL, wrapper)
return decorator
def on_joined_world(self):
def decorator(fun):
@functool.wraps(fun)
@functools.wraps(fun)
async def wrapper():
event = JoinGameEvent()
return await fun(event)
return self.register(JoinGameEvent.SENTINEL, callback)
return self.register(JoinGameEvent.SENTINEL, wrapper)
return decorator
def __init__(self, *args, **kwargs):

View file

@ -1,6 +1,6 @@
from typing import Callable, List
class Notifier:
class Notifier: # TODO this should be an Addon too!
_report_functions : List[Callable]
def __init__(self):
@ -16,9 +16,9 @@ class Notifier:
def notify(self, text, log:bool = False, **kwargs):
print(text)
async def initialize(self, _client:'Treepuncher'):
async def initialize(self):
pass
async def cleanup(self, _client:'Treepuncher'):
async def cleanup(self):
pass

View file

@ -1,3 +1,4 @@
import os
import json
import sqlite3
@ -16,7 +17,10 @@ class Storage:
def __init__(self, name:str):
self.name = name
init = not os.path.isfile(f"{name}.session")
self.db = sqlite3.connect(f'{name}.session')
if init:
self._init_db()
def __del__(self):
self.close()

View file

@ -7,7 +7,8 @@ import uuid
from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints
from enum import Enum
from dataclasses import dataclass
from time import time
from dataclasses import dataclass, MISSING
from configparser import ConfigParser
from apscheduler.schedulers.asyncio import AsyncIOScheduler
@ -27,12 +28,12 @@ class ConfigObject:
class Addon:
name : str
config : ConfigObject
_client : 'Treepuncher'
@dataclass(frozen=True)
class Options(ConfigObject):
pass
config : Options
@property
def client(self) -> 'Treepuncher':
@ -43,21 +44,25 @@ class Addon:
self.name = type(self).__name__
cfg = self._client.config
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:
opts[name] = self._client.config[self.name].getboolean(name)
else:
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
opts[name] = default
cfg_clazz = get_type_hints(type(self))['config']
if cfg_clazz is not ConfigObject:
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
if cfg.has_option(self.name, name):
if field.type is bool:
opts[name] = self._client.config[self.name].getboolean(name)
else:
opts[name] = field.type(self._client.config[self.name].get(name))
elif default is MISSING:
raise ValueError(f"Missing required value '{name}' of type '{field.type.__name__}' in section '{self.name}'")
else: # not really necessary since it's a dataclass but whatever
opts[name] = default
self.config = self.Options(**opts)
self.register(client)
self.register()
def register(self, client:'Treepuncher'):
def register(self):
pass
async def initialize(self):
@ -82,25 +87,26 @@ class Treepuncher(
modules : List[Addon]
ctx : Dict[Any, Any]
def __init__(self, name:str, *args, config_file:str=None, **kwargs):
def __init__(self, name:str, *args, config_file:str=None, notifier:Notifier=None, **kwargs):
super().__init__(*args, **kwargs)
self.ctx = dict()
self.name = name
self.config = ConfigParser()
config_path = config_file or f'config-{self.name}.ini'
config_path = config_file or f'{self.name}.ini'
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._logger.warning("Saved credentials are not from this session")
self._authenticator.deserialize(json.loads(prev.token))
self._logger.info("Loaded credentials")
self.modules = []
# self.notifier = notifier or Notifier()
self.notifier = notifier or Notifier()
tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # APScheduler will complain if I don't specify a timezone...
self.scheduler = AsyncIOScheduler(timezone=tz)
logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) # So it's way less spammy
@ -127,9 +133,9 @@ class Treepuncher(
self.storage._set_state(state)
async def start(self):
await self.notifier.initialize(self)
await self.notifier.initialize()
for m in self.modules:
await m.initialize(self)
await m.initialize()
await super().start()
self.scheduler.resume()
@ -138,12 +144,8 @@ class Treepuncher(
await super().stop(force=force)
for m in self.modules:
await m.cleanup()
await self.notifier.cleanup(self)
await self.notifier.cleanup()
def install(self, module:Type[Addon]) -> Type[Addon]:
self.modules.append(module(self))
return module
async def write(self, packet:Packet, wait:bool=False):
await self.dispatcher.write(packet, wait)