diff --git a/src/treepuncher/storage.py b/src/treepuncher/storage.py index 1a292a3..d5617ca 100644 --- a/src/treepuncher/storage.py +++ b/src/treepuncher/storage.py @@ -3,18 +3,28 @@ import json import sqlite3 from dataclasses import dataclass -from typing import Optional, Any +from typing import Optional, Any, Dict +from datetime import datetime + +__DATE_FORMAT__ : str = "%Y-%m-%d %H:%M:%S.%f" @dataclass class SystemState: name : str - token : str + version : str start_time : int +@dataclass +class AuthenticatorState: + date : datetime + token : Dict[str, Any] + legacy : bool = False + class Storage: name : str db : sqlite3.Connection + def __init__(self, name:str): self.name = name init = not os.path.isfile(f"{name}.session") @@ -30,14 +40,21 @@ class Storage: def _init_db(self): cur = self.db.cursor() - cur.execute('CREATE TABLE system (name TEXT, token TEXT, start_time LONG)') - cur.execute('CREATE TABLE documents (name TEXT, value TEXT)') + cur.execute('CREATE TABLE system (name TEXT PRIMARY KEY, version TEXT, start_time LONG)') + cur.execute('CREATE TABLE documents (name TEXT PRIMARY KEY, value TEXT)') + cur.execute('CREATE TABLE authenticator (date TEXT PRIMARY KEY, token TEXT, legacy BOOL') self.db.commit() def _set_state(self, state:SystemState): cur = self.db.cursor() cur.execute('DELETE FROM system') - cur.execute('INSERT INTO system VALUES (?, ?, ?)', (state.name, state.token, state.start_time)) + cur.execute('INSERT INTO system VALUES (?, ?, ?)', (state.name, state.version, int(state.start_time))) + self.db.commit() + + def _set_auth(self, state:AuthenticatorState): + cur = self.db.cursor() + cur.execute('DELETE FROM authenticator') + cur.execute('INSERT INTO authenticator VALUES (?, ?, ?)', (state.date.strftime(__DATE_FORMAT__), json.dumps(state.token), state.legacy)) self.db.commit() def system(self) -> Optional[SystemState]: @@ -47,10 +64,21 @@ class Storage: return None return SystemState( name=val[0][0], - token=val[0][1], + version=val[0][1], start_time=val[0][2] ) + def auth(self) -> Optional[AuthenticatorState]: + cur = self.db.cursor() + val = cur.execute('SELECT * FROM authenticator').fetchall() + if not val: + return None + return AuthenticatorState( + date=datetime.strptime(val[0][0], __DATE_FORMAT__), + token=json.loads(val[0][1]), + legacy=val[0][2] or False + ) + def get(self, key:str) -> Optional[Any]: cur = self.db.cursor() val = cur.execute("SELECT * FROM documents WHERE name = ?", (key,)).fetchall() diff --git a/src/treepuncher/treepuncher.py b/src/treepuncher/treepuncher.py index 13321f3..9fd3961 100644 --- a/src/treepuncher/treepuncher.py +++ b/src/treepuncher/treepuncher.py @@ -4,6 +4,7 @@ import logging import asyncio import datetime import uuid +import pkg_resources from typing import List, Dict, Optional, Any, Type, get_args, get_origin, get_type_hints, Set, Callable from time import time @@ -15,9 +16,11 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from aiocraft.mc.packet import Packet from aiocraft.mc.auth import AuthInterface, AuthException, MojangAuthenticator, MicrosoftAuthenticator, OfflineAuthenticator -from .storage import Storage, SystemState +from .storage import Storage, SystemState, AuthenticatorState from .game import GameState, GameChat, GameInventory, GameTablist, GameWorld +__VERSION__ = pkg_resources.get_distribution('treepuncher').version + def parse_with_hint(val:str, hint:Any) -> Any: if hint is bool: if val.lower() in ['1', 'true', 't', 'on', 'enabled']: @@ -181,11 +184,20 @@ class Treepuncher( super().__init__(opt('server', required=True), online_mode=online_mode, authenticator=authenticator) prev = self.storage.system() # if this isn't 1st time, this won't be None. Load token from there + state = SystemState(self.name, __VERSION__, 0) if prev: + state.start_time = prev.start_time if self.name != prev.name: self.logger.warning("Saved session belong to another user") - authenticator.deserialize(json.loads(prev.token)) - self.logger.info("Loaded authenticated session") + if prev.version != state.version: + self.logger.warning("Saved session uses a different version") + prev_auth = self.storage.auth() + if prev_auth: + if prev_auth.legacy ^ isinstance(authenticator, MicrosoftAuthenticator): + self.logger.warning("Saved session is incompatible with configured authenticator") + authenticator.deserialize(prev_auth.token) + self.logger.info("Loaded session from %s", prev_auth.date) + self.storage._set_state(state) @property @@ -194,12 +206,12 @@ class Treepuncher( async def authenticate(self): await super().authenticate() - state = SystemState( - name=self.name, - token=json.dumps(self.authenticator.serialize()), - start_time=int(time()) + state = AuthenticatorState( + date=datetime.datetime.now(), + token=self.authenticator.serialize(), + legacy=isinstance(self.authenticator, MojangAuthenticator) ) - self.storage._set_state(state) + self.storage._set_auth(state) async def start(self): # if self.started: # TODO readd check @@ -213,6 +225,7 @@ class Treepuncher( self._worker = asyncio.get_event_loop().create_task(self._work()) self.scheduler.resume() self.logger.info("Treepuncher started") + self.storage._set_state(SystemState(self.name, __VERSION__, time())) async def stop(self, force: bool = False): self._processing = False