reworked initialization

tldr now treepuncher handles initializing everything
This commit is contained in:
əlemi 2022-04-18 19:35:49 +02:00
parent c9f65f56aa
commit af4cf7db90
No known key found for this signature in database
GPG key ID: BBCBFE5D7244634E
2 changed files with 85 additions and 78 deletions

View file

@ -53,16 +53,17 @@ 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('-c', '--code', dest='code', action='store_const', const=True, default=False, help="request new code to login") parser.add_argument('--server', dest='server', default='', help='server to connect to')
parser.add_argument('--client-id', dest='cid', default='c63ef189-23cb-453b-8060-13800b85d2dc', help='client_id of your Azure application') parser.add_argument('--debug', dest='_debug', action='store_const', const=True, default=False, help="enable debug logs")
parser.add_argument('--secret', dest='secret', default='N2e7Q~ybYA0IO39KB1mFD4GmoYzISRaRNyi59', help='client_secret of your Azure application') 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('--redirect-uri', dest='uri', default='https://fantabos.co/msauth', help='redirect_uri of your Azure application')
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('--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-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('--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') 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 # 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) 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 kwargs = {}
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--> ") if args.server:
kwargs["server"] = args.server
client = Treepuncher( client = Treepuncher(
args.name, args.name,
args.server, args.server,
use_packet_whitelist=args.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) enabled_addons = set(a.lower() for a in args.add)

View file

@ -5,40 +5,32 @@ import asyncio
import datetime import datetime
import uuid import uuid
from typing import List, Dict, Tuple, Union, Optional, Any, Type, get_type_hints from typing import List, Dict, Optional, Any, Type, get_type_hints
from enum import Enum
from time import time from time import time
from dataclasses import dataclass, MISSING 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
from aiocraft.client import MinecraftClient
from aiocraft.util import helpers
from aiocraft.mc.packet import Packet from aiocraft.mc.packet import Packet
from aiocraft.mc.definitions import ConnectionState from aiocraft.mc.auth import AuthInterface, AuthException, MojangAuthenticator, MicrosoftAuthenticator, OfflineAuthenticator
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 .scaffold import Scaffold
from .events import ConnectedEvent, DisconnectedEvent
from .storage import Storage, SystemState 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
from .traits import CallbacksHolder, Runnable
REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]") REMOVE_COLOR_FORMATS = re.compile(r"§[0-9a-z]")
class ConfigObject: class ConfigObject:
def __getitem__(self, key:str) -> Any: def __getitem__(self, key: str) -> Any:
return getattr(self, key) return getattr(self, key)
class Addon: class Addon:
name : str name: str
config : ConfigObject config: ConfigObject
_client : 'Treepuncher' _client: 'Treepuncher'
@dataclass(frozen=True) @dataclass(frozen=True)
class Options(ConfigObject): class Options(ConfigObject):
@ -48,11 +40,11 @@ class Addon:
def client(self) -> 'Treepuncher': def client(self) -> 'Treepuncher':
return self._client return self._client
def __init__(self, client:'Treepuncher', *args, **kwargs): 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
opts : Dict[str, Any] = {} opts: Dict[str, Any] = {}
cfg_clazz = get_type_hints(type(self))['config'] cfg_clazz = get_type_hints(type(self))['config']
if cfg_clazz is not ConfigObject: if cfg_clazz is not ConfigObject:
for name, field in cfg_clazz.__dataclass_fields__.items(): for name, field in cfg_clazz.__dataclass_fields__.items():
@ -65,8 +57,10 @@ class Addon:
else: else:
opts[name] = field.type(self._client.config[self.name].get(name)) opts[name] = field.type(self._client.config[self.name].get(name))
elif default is MISSING: elif default is MISSING:
raise ValueError(f"Missing required value '{name}' of type '{field.type.__name__}' in section '{self.name}'") raise ValueError(
else: # not really necessary since it's a dataclass but whatever 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 opts[name] = default
self.config = self.Options(**opts) self.config = self.Options(**opts)
self.register() self.register()
@ -80,35 +74,34 @@ class Addon:
async def cleanup(self): async def cleanup(self):
pass pass
class Treepuncher( class Treepuncher(
GameState, GameState,
GameChat, GameChat,
GameInventory, GameInventory,
GameTablist, GameTablist,
GameWorld GameWorld
): ):
name : str name: str
config : ConfigParser config: ConfigParser
storage : Storage storage: Storage
notifier : Notifier notifier: Notifier
scheduler : AsyncIOScheduler scheduler: AsyncIOScheduler
modules : List[Addon] modules: List[Addon]
ctx : Dict[Any, Any] ctx: Dict[Any, Any]
_processing : bool _processing: bool
def __init__( def __init__(
self, self,
name:str, name: str,
server:str, config_file: str = None,
config_file:str=None, online_mode: bool = True,
notifier:Notifier=None, legacy: bool = False,
online_mode:bool=True, notifier: Notifier = None,
authenticator:Optional[AuthInterface]=None,
**kwargs **kwargs
): ):
super().__init__(server, online_mode=online_mode, authenticator=authenticator, username=name)
self.ctx = dict() self.ctx = dict()
self.name = name self.name = name
@ -116,32 +109,48 @@ class Treepuncher(
config_path = config_file or f'{self.name}.ini' config_path = config_file or f'{self.name}.ini'
self.config.read(config_path) 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) 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 credentials are not from this session") self.logger.warning("Saved credentials belong to another session")
self._authenticator.deserialize(json.loads(prev.token)) authenticator.deserialize(json.loads(prev.token))
self._logger.info("Loaded credentials") 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() # This doesn't work anymore
self.scheduler = AsyncIOScheduler(timezone=tz) self.scheduler = AsyncIOScheduler() # TODO APScheduler warns about timezone ugghh
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
self.scheduler.start(paused=True) self.scheduler.start(paused=True)
super().__init__(opt('server'), online_mode=online_mode, authenticator=authenticator)
@property @property
def playerName(self) -> str: def playerName(self) -> str:
if self.online_mode: return self.authenticator.selectedProfile.name
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")
async def authenticate(self): async def authenticate(self):
await super().authenticate() await super().authenticate()
@ -162,9 +171,9 @@ class Treepuncher(
self._processing = True self._processing = True
self._worker = asyncio.get_event_loop().create_task(self._work()) self._worker = asyncio.get_event_loop().create_task(self._work())
self.scheduler.resume() 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._processing = False
self.scheduler.pause() self.scheduler.pause()
if self.dispatcher.connected: if self.dispatcher.connected:
@ -176,9 +185,9 @@ class Treepuncher(
await m.cleanup() await m.cleanup()
await self.notifier.cleanup() await self.notifier.cleanup()
await super().stop() 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)) self.modules.append(module(self))
return module return module
@ -186,7 +195,7 @@ class Treepuncher(
try: try:
server_data = await self.info(host=self.host, port=self.port) server_data = await self.info(host=self.host, port=self.port)
except Exception: except Exception:
return self._logger.exception("exception while pinging server") return self.logger.exception("exception while pinging server")
while self._processing: while self._processing:
try: try:
await self.join( await self.join(
@ -196,12 +205,12 @@ class Treepuncher(
packet_whitelist=self.callback_keys(filter=Packet), packet_whitelist=self.callback_keys(filter=Packet),
) )
except ConnectionRefusedError: except ConnectionRefusedError:
self._logger.error("Server rejected connection") self.logger.error("Server rejected connection")
except OSError as e: except OSError as e:
self._logger.error("Connection error : %s", str(e)) self.logger.error("Connection error : %s", str(e))
except Exception: except Exception:
self._logger.exception("Unhandled exception") self.logger.exception("Unhandled exception")
break break
await asyncio.sleep(5) # TODO setting await asyncio.sleep(5) # TODO setting
if self._processing: if self._processing:
await self.stop(force=True) await self.stop(force=True)