diff --git a/api.py b/api.py new file mode 100644 index 0000000..4480f20 --- /dev/null +++ b/api.py @@ -0,0 +1,157 @@ +import asyncio, queue, json, threading, hashlib, websockets, inspect +from websockets.asyncio.client import connect as wsconnect + +class BasicServerConnection(): + def __init__(self,uri: str) -> None: + self.uri = uri + self.callbacks = {} + self.resq = queue.Queue() + self.asklock = asyncio.Lock() + self.needsClose = threading.Event() + + def addCallback(self,name,func): + # print("addcallback",name,func) + if name in self.callbacks: + self.callbacks[name].append(func) + else: + self.callbacks[name] = [func] + + def removeCallback(self,name,func): + if not name in self.callbacks: return + self.callbacks[name].remove(func) + + async def __runCallback(self,name,*args): + # print("runcallback",name,args) + if not name in self.callbacks: return + for func in self.callbacks[name]: + # print(func) + if inspect.iscoroutinefunction(func): + await func(*args) + else: + func(*args) + + def __runCallbackInThread(self,name,*args): + def __run(): + asyncio.run(self.__runCallback(name,*args)) + thread = threading.Thread(target=__run) + thread.start() + + async def connect(self) -> None: + uri = self.uri + async with wsconnect(uri) as ws: + self.ws = ws + while True: + if self.needsClose.is_set(): + await self.ws.close() + break + try: + txt = await asyncio.wait_for(ws.recv(), timeout=0.1) + # print("recv",txt) + res = json.loads(txt) + if "connected" == res["type"]: + self.__runCallbackInThread("connect") + if res["type"] in ["response","error"]: + self.resq.put(res) + except websockets.exceptions.ConnectionClosed: + break + except TimeoutError: + pass + except Exception as e: + pass + + async def disconnect(self) -> None: + if not self.ws: return + self.needsClose.set() + await self.__runCallback("connect") + + async def waitConnect(self) -> None: + # wait for it to reconnect + # print("waiting for reconnection.") + event = threading.Event() + def evset(): + event.set() + self.addCallback("connect",evset) + self.addCallback("disconnect",evset) + #self.callbacks["connect"].insert(0,evset) + await asyncio.to_thread(event.wait) + # print("reconnected!! hell yeah") + self.removeCallback("connect",evset) + self.removeCallback("disconnect",evset) + + async def ask(self,obj: dict,t:int=None) -> dict | None: + # print("ask",obj) + while True: + if self.needsClose.is_set(): + return None + try: + async with self.asklock: + await self.ws.send(json.dumps(obj)) + e = self.resq.get(timeout=t) + except queue.Empty: + e = None + except websockets.exceptions.ConnectionClosed: + await self.waitConnect() + continue + return e + +class ServerConnection(BasicServerConnection): + def __init__(self,uri: str = "ws://localhost:30341"): + super().__init__(uri) + self.logincmd = None + super().addCallback("connect",self.__login) + + async def __connectloop(self): + self.mustConnect = True + first = True + while self.mustConnect: + if not first: + await asyncio.sleep(1) + first=False + try: + await super().connect() + except OSError: + pass + + async def connect(self): + self.thread = threading.Thread(target=lambda: asyncio.run(self.__connectloop())) + self.thread.start() + await super().waitConnect() + + async def disconnect(self): + self.mustConnect = False + await super().disconnect() + self.thread.join() + + async def status(self): + return await super().ask({"type": "status"}) + + async def __login(self): + # print("__login") + if self.logincmd == None: return {"type": "error", "error": "No credentials found.", "server": False} + stat = await self.status() + if stat==None: return + if stat["type"]=="error" or stat["response"]["login"]==False: + return await super().ask(self.logincmd) + + async def login(self,name: str,pwrd: str): + res = hashlib.md5(pwrd.encode("utf8")) + psum = res.hexdigest() + self.logincmd = {"type": "login", "name": name, "pass": psum} + return await self.__login() + + async def logout(self): + self.logincmd = None + return await super().ask({"type": "logout"}) + + async def createuser(self,name: str,pwrd: str): + res = hashlib.md5(pwrd.encode("utf8")) + psum = res.hexdigest() + return await super().ask({"type": "createuser", "name": name, "pass": psum}) + + async def changename(self,name: str): + return await super().ask({"type": "changename", "name": name}) + + async def changepass(self,old: str,new: str): + osum = hashlib.md5(old.encode("utf8")).hexdigest() + nsum = hashlib.md5(new.encode("utf8")).hexdigest() + return await super().ask({"type": "changepass", "oldpass": osum, "newpass": nsum})