reworked initialization
tldr now treepuncher handles initializing everything
This commit is contained in:
parent
c9f65f56aa
commit
af4cf7db90
2 changed files with 85 additions and 78 deletions
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue