From af4cf7db90bd5616d3d1ef900a8aced79f74eb13 Mon Sep 17 00:00:00 2001 From: alemidev Date: Mon, 18 Apr 2022 19:35:49 +0200 Subject: [PATCH] reworked initialization tldr now treepuncher handles initializing everything --- src/treepuncher/__main__.py | 26 +++---- src/treepuncher/treepuncher.py | 137 ++++++++++++++++++--------------- 2 files changed, 85 insertions(+), 78 deletions(-) diff --git a/src/treepuncher/__main__.py b/src/treepuncher/__main__.py index 5f46101..a8b734a 100644 --- a/src/treepuncher/__main__.py +++ b/src/treepuncher/__main__.py @@ -53,16 +53,17 @@ 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') + + parser.add_argument('--server', dest='server', default='', help='server to connect to') + 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('--code', dest='code', default='', help='login code for oauth2 flow') + parser.add_argument('--mojang', dest='mojang', action='store_const', const=True, default=False, help="use legacy Mojang authenticator") + parser.add_argument('--addon-path', dest='path', default='', help='path for loading addons') 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-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 @@ -71,18 +72,15 @@ def main(): 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--> ") + kwargs = {} + + if args.server: + kwargs["server"] = args.server client = Treepuncher( args.name, args.server, 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) diff --git a/src/treepuncher/treepuncher.py b/src/treepuncher/treepuncher.py index 73345b6..c24d2e2 100644 --- a/src/treepuncher/treepuncher.py +++ b/src/treepuncher/treepuncher.py @@ -5,40 +5,32 @@ import asyncio import datetime import uuid -from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints -from enum import Enum +from typing import List, Dict, Optional, Any, Type, get_type_hints from time import time from dataclasses import dataclass, MISSING from configparser import ConfigParser from apscheduler.schedulers.asyncio import AsyncIOScheduler -from aiocraft.client import MinecraftClient -from aiocraft.util import helpers from aiocraft.mc.packet import Packet -from aiocraft.mc.definitions import ConnectionState -from aiocraft.mc.auth import AuthInterface, MicrosoftAuthenticator, MojangAuthenticator -from aiocraft.mc.proto import PacketSetCompression, PacketKickDisconnect -from aiocraft.mc.proto.play.clientbound import PacketKeepAlive -from aiocraft.mc.proto.play.serverbound import PacketKeepAlive as PacketKeepAliveResponse +from aiocraft.mc.auth import AuthInterface, AuthException, MojangAuthenticator, MicrosoftAuthenticator, OfflineAuthenticator -from .scaffold import Scaffold -from .events import ConnectedEvent, DisconnectedEvent from .storage import Storage, SystemState from .notifier import Notifier from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld -from .traits import CallbacksHolder, Runnable REMOVE_COLOR_FORMATS = re.compile(r"ยง[0-9a-z]") + class ConfigObject: - def __getitem__(self, key:str) -> Any: + def __getitem__(self, key: str) -> Any: return getattr(self, key) + class Addon: - name : str - config : ConfigObject - _client : 'Treepuncher' + name: str + config: ConfigObject + _client: 'Treepuncher' @dataclass(frozen=True) class Options(ConfigObject): @@ -48,11 +40,11 @@ class Addon: def client(self) -> 'Treepuncher': return self._client - def __init__(self, client:'Treepuncher', *args, **kwargs): + def __init__(self, client: 'Treepuncher', *args, **kwargs): self._client = client self.name = type(self).__name__ cfg = self._client.config - opts : Dict[str, Any] = {} + opts: Dict[str, Any] = {} cfg_clazz = get_type_hints(type(self))['config'] if cfg_clazz is not ConfigObject: for name, field in cfg_clazz.__dataclass_fields__.items(): @@ -65,8 +57,10 @@ class Addon: 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 + 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() @@ -80,35 +74,34 @@ class Addon: async def cleanup(self): pass + class Treepuncher( - GameState, - GameChat, - GameInventory, - GameTablist, - GameWorld + GameState, + GameChat, + GameInventory, + GameTablist, + GameWorld ): - name : str - config : ConfigParser - storage : Storage + name: str + config: ConfigParser + storage: Storage - notifier : Notifier - scheduler : AsyncIOScheduler - modules : List[Addon] - ctx : Dict[Any, Any] + notifier: Notifier + scheduler: AsyncIOScheduler + modules: List[Addon] + ctx: Dict[Any, Any] - _processing : bool + _processing: bool def __init__( self, - name:str, - server:str, - config_file:str=None, - notifier:Notifier=None, - online_mode:bool=True, - authenticator:Optional[AuthInterface]=None, + name: str, + config_file: str = None, + online_mode: bool = True, + legacy: bool = False, + notifier: Notifier = None, **kwargs ): - super().__init__(server, online_mode=online_mode, authenticator=authenticator, username=name) self.ctx = dict() self.name = name @@ -116,32 +109,48 @@ class Treepuncher( config_path = config_file or f'{self.name}.ini' self.config.read(config_path) + authenticator : AuthInterface + + def opt(k:str) -> Any: + return kwargs.get(k) or self.config['Treepuncher'].get(k) + + if not online_mode: + authenticator = OfflineAuthenticator(self.name) + elif legacy: + authenticator = MojangAuthenticator( + username= opt('username') or name, + password= opt('password') + ) + else: + authenticator = MicrosoftAuthenticator( + client_id= opt('client_id'), + client_secret= opt('client_secret'), + redirect_uri= opt('redirect_uri'), + code= opt('code'), + ) + 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 self.name != prev.name: - self._logger.warning("Saved credentials are not from this session") - self._authenticator.deserialize(json.loads(prev.token)) - self._logger.info("Loaded credentials") + self.logger.warning("Saved credentials belong to another session") + authenticator.deserialize(json.loads(prev.token)) + self.logger.info("Loaded credentials") self.modules = [] 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 + # tz = datetime.datetime.now(datetime.timezone.utc).astimezone().tzname() # This doesn't work anymore + self.scheduler = AsyncIOScheduler() # TODO APScheduler warns about timezone ugghh + logging.getLogger('apscheduler.executors.default').setLevel(logging.WARNING) # So it's way less spammy self.scheduler.start(paused=True) + super().__init__(opt('server'), online_mode=online_mode, authenticator=authenticator) + + @property def playerName(self) -> str: - if self.online_mode: - if self._authenticator and self._authenticator.selectedProfile: - return self._authenticator.selectedProfile.name - raise ValueError("Username unknown: client not authenticated") - else: - if self._username: - return self._username - raise ValueError("No username configured for offline mode") + return self.authenticator.selectedProfile.name async def authenticate(self): await super().authenticate() @@ -162,9 +171,9 @@ class Treepuncher( self._processing = True self._worker = asyncio.get_event_loop().create_task(self._work()) self.scheduler.resume() - self._logger.info("Treepuncher started") + self.logger.info("Treepuncher started") - async def stop(self, force:bool=False): + async def stop(self, force: bool = False): self._processing = False self.scheduler.pause() if self.dispatcher.connected: @@ -176,9 +185,9 @@ class Treepuncher( await m.cleanup() await self.notifier.cleanup() await super().stop() - self._logger.info("Treepuncher stopped") + self.logger.info("Treepuncher stopped") - def install(self, module:Type[Addon]) -> Type[Addon]: + def install(self, module: Type[Addon]) -> Type[Addon]: self.modules.append(module(self)) return module @@ -186,7 +195,7 @@ class Treepuncher( try: server_data = await self.info(host=self.host, port=self.port) except Exception: - return self._logger.exception("exception while pinging server") + return self.logger.exception("exception while pinging server") while self._processing: try: await self.join( @@ -196,12 +205,12 @@ class Treepuncher( packet_whitelist=self.callback_keys(filter=Packet), ) except ConnectionRefusedError: - self._logger.error("Server rejected connection") + self.logger.error("Server rejected connection") except OSError as e: - self._logger.error("Connection error : %s", str(e)) + self.logger.error("Connection error : %s", str(e)) except Exception: - self._logger.exception("Unhandled exception") + self.logger.exception("Unhandled exception") break - await asyncio.sleep(5) # TODO setting + await asyncio.sleep(5) # TODO setting if self._processing: await self.stop(force=True)