Various fixes, implemented credentials storage and addons with config vars
This commit is contained in:
parent
d5934da832
commit
d12b4b4f88
5 changed files with 49 additions and 35 deletions
|
@ -36,6 +36,8 @@ def main():
|
||||||
for addon in addons:
|
for addon in addons:
|
||||||
help_text += f"\n {addon.__name__} \t{addon.__doc__ or ''}"
|
help_text += f"\n {addon.__name__} \t{addon.__doc__ or ''}"
|
||||||
cfg_clazz = get_type_hints(addon)['config']
|
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():
|
for name, field in cfg_clazz.__dataclass_fields__.items():
|
||||||
default = field.default if field.default is not MISSING \
|
default = field.default if field.default is not MISSING \
|
||||||
else field.default_factory() if field.default_factory 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('--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-filter', dest='use_packet_whitelist', action='store_const', const=False, default=True, help="disable packet whitelist, will decrease performance")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
@ -74,15 +78,18 @@ def main():
|
||||||
client = Treepuncher(
|
client = Treepuncher(
|
||||||
args.name,
|
args.name,
|
||||||
args.server,
|
args.server,
|
||||||
use_packet_whitelist=use_packet_whitelist,
|
use_packet_whitelist=args.use_packet_whitelist,
|
||||||
login_code=code,
|
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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enabled_addons = set(a.lower() for a in args.add)
|
||||||
for addon in addons:
|
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()
|
client.run()
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import datetime
|
import datetime
|
||||||
|
import functools
|
||||||
|
|
||||||
from aiocraft.client import MinecraftClient
|
from aiocraft.client import MinecraftClient
|
||||||
from aiocraft.mc.definitions import Gamemode, Dimension, Difficulty
|
from aiocraft.mc.definitions import Gamemode, Dimension, Difficulty
|
||||||
|
@ -27,20 +28,20 @@ class GameState(MinecraftClient):
|
||||||
|
|
||||||
def on_death(self):
|
def on_death(self):
|
||||||
def decorator(fun):
|
def decorator(fun):
|
||||||
@functool.wraps(fun)
|
@functools.wraps(fun)
|
||||||
async def wrapper():
|
async def wrapper():
|
||||||
event = DeathEvent()
|
event = DeathEvent()
|
||||||
return await fun(event)
|
return await fun(event)
|
||||||
return self.register(DeathEvent.SENTINEL, fun)
|
return self.register(DeathEvent.SENTINEL, wrapper)
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def on_joined_world(self):
|
def on_joined_world(self):
|
||||||
def decorator(fun):
|
def decorator(fun):
|
||||||
@functool.wraps(fun)
|
@functools.wraps(fun)
|
||||||
async def wrapper():
|
async def wrapper():
|
||||||
event = JoinGameEvent()
|
event = JoinGameEvent()
|
||||||
return await fun(event)
|
return await fun(event)
|
||||||
return self.register(JoinGameEvent.SENTINEL, callback)
|
return self.register(JoinGameEvent.SENTINEL, wrapper)
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
from typing import Callable, List
|
from typing import Callable, List
|
||||||
|
|
||||||
class Notifier:
|
class Notifier: # TODO this should be an Addon too!
|
||||||
_report_functions : List[Callable]
|
_report_functions : List[Callable]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
@ -16,9 +16,9 @@ class Notifier:
|
||||||
def notify(self, text, log:bool = False, **kwargs):
|
def notify(self, text, log:bool = False, **kwargs):
|
||||||
print(text)
|
print(text)
|
||||||
|
|
||||||
async def initialize(self, _client:'Treepuncher'):
|
async def initialize(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def cleanup(self, _client:'Treepuncher'):
|
async def cleanup(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import os
|
||||||
import json
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
|
|
||||||
|
@ -16,7 +17,10 @@ class Storage:
|
||||||
|
|
||||||
def __init__(self, name:str):
|
def __init__(self, name:str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
init = not os.path.isfile(f"{name}.session")
|
||||||
self.db = sqlite3.connect(f'{name}.session')
|
self.db = sqlite3.connect(f'{name}.session')
|
||||||
|
if init:
|
||||||
|
self._init_db()
|
||||||
|
|
||||||
def __del__(self):
|
def __del__(self):
|
||||||
self.close()
|
self.close()
|
||||||
|
|
|
@ -7,7 +7,8 @@ import uuid
|
||||||
|
|
||||||
from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints
|
from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from dataclasses import dataclass
|
from time import time
|
||||||
|
from dataclasses import dataclass, MISSING
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
|
@ -27,12 +28,12 @@ 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':
|
||||||
|
@ -43,21 +44,25 @@ class Addon:
|
||||||
self.name = type(self).__name__
|
self.name = type(self).__name__
|
||||||
cfg = self._client.config
|
cfg = self._client.config
|
||||||
opts : Dict[str, Any] = {}
|
opts : Dict[str, Any] = {}
|
||||||
for name, clazz in get_type_hints(self.Options).items():
|
cfg_clazz = get_type_hints(type(self))['config']
|
||||||
default = getattr(self.Options, name, None)
|
if cfg_clazz is not ConfigObject:
|
||||||
if cfg.has_option(self.name, name):
|
for name, field in cfg_clazz.__dataclass_fields__.items():
|
||||||
if clazz is bool:
|
default = field.default if field.default is not MISSING \
|
||||||
opts[name] = self._client.config[self.name].getboolean(name)
|
else field.default_factory() if field.default_factory is not MISSING \
|
||||||
else:
|
else MISSING
|
||||||
opts[name] = clazz(self._client.config[self.name].get(name))
|
if cfg.has_option(self.name, name):
|
||||||
elif default is None:
|
if field.type is bool:
|
||||||
raise ValueError(f"Missing required value '{name}' of type '{clazz.__name__}' in section '{self.name}'")
|
opts[name] = self._client.config[self.name].getboolean(name)
|
||||||
else: # not really necessary since it's a dataclass but whatever
|
else:
|
||||||
opts[name] = default
|
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.config = self.Options(**opts)
|
||||||
self.register(client)
|
self.register()
|
||||||
|
|
||||||
def register(self, client:'Treepuncher'):
|
def register(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def initialize(self):
|
async def initialize(self):
|
||||||
|
@ -82,25 +87,26 @@ class Treepuncher(
|
||||||
modules : List[Addon]
|
modules : List[Addon]
|
||||||
ctx : Dict[Any, Any]
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
self.ctx = dict()
|
self.ctx = dict()
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.config = ConfigParser()
|
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.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
|
prev = self.storage.system() # if this isn't 1st time, this won't be None. Load token from there
|
||||||
if prev:
|
if prev:
|
||||||
if self.name != prev.name:
|
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._authenticator.deserialize(json.loads(prev.token))
|
||||||
|
self._logger.info("Loaded credentials")
|
||||||
|
|
||||||
self.modules = []
|
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...
|
tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # APScheduler will complain if I don't specify a timezone...
|
||||||
self.scheduler = AsyncIOScheduler(timezone=tz)
|
self.scheduler = AsyncIOScheduler(timezone=tz)
|
||||||
logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) # So it's way less spammy
|
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)
|
self.storage._set_state(state)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
await self.notifier.initialize(self)
|
await self.notifier.initialize()
|
||||||
for m in self.modules:
|
for m in self.modules:
|
||||||
await m.initialize(self)
|
await m.initialize()
|
||||||
await super().start()
|
await super().start()
|
||||||
self.scheduler.resume()
|
self.scheduler.resume()
|
||||||
|
|
||||||
|
@ -138,12 +144,8 @@ class Treepuncher(
|
||||||
await super().stop(force=force)
|
await super().stop(force=force)
|
||||||
for m in self.modules:
|
for m in self.modules:
|
||||||
await m.cleanup()
|
await m.cleanup()
|
||||||
await self.notifier.cleanup(self)
|
await self.notifier.cleanup()
|
||||||
|
|
||||||
def install(self, module:Type[Addon]) -> Type[Addon]:
|
def install(self, module:Type[Addon]) -> Type[Addon]:
|
||||||
self.modules.append(module(self))
|
self.modules.append(module(self))
|
||||||
return module
|
return module
|
||||||
|
|
||||||
async def write(self, packet:Packet, wait:bool=False):
|
|
||||||
await self.dispatcher.write(packet, wait)
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue