diff --git a/main.py b/main.py index 193dcf3..3969df4 100644 --- a/main.py +++ b/main.py @@ -43,6 +43,28 @@ async def handler(request: aiohttp.web.Request): print(f"{client=} accessed") return aiohttp.web.FileResponse('ui.html') +@routes.post('/api/token{authtoken}/{method}') +async def handler(request: aiohttp.web.Request): + method = request.match_info.get('method', None) + token = request.match_info.get('authtoken', None) + model = request.app['model'] + + data = await request.json() + + try: + assert method in model.ApiMethod.dict + value = await model.ApiMethod.dict[method](model, token, **data) + if value: + return aiohttp.web.json_response(value) + else: + return aiohttp.web.Response(status=200) + except Exception as e: + del e # unused? + traceback.print_exc() + return aiohttp.web.Response(status=400) + #finally: + # await model.send_state(client) + @routes.post('/api/{method}') async def handler(request: aiohttp.web.Request): @@ -52,7 +74,6 @@ async def handler(request: aiohttp.web.Request): data = await request.json() - print(f'{method=} {data=}') try: assert method in model.ApiMethod.dict @@ -68,26 +89,6 @@ async def handler(request: aiohttp.web.Request): #finally: # await model.send_state(client) -@routes.get(CLIENT_REGEX + '/ws') -async def _(request: aiohttp.web.Request): - client = get_client(request) - model = request.app['model'] - - ws = aiohttp.web.WebSocketResponse(heartbeat=10) - await ws.prepare(request) - - print(f'[WS] client {client} connected, {ws=}') - await model.subscribe(client, ws) - - async for msg in ws: - print(f'[WS] incoming message from client {client}, {ws=}, {msg=}') - - print(f'[WS] client {client} disconnected, {ws=}') - model.unsubscribe(ws) - - return ws - - @routes.get(f"/api/{admintoken}") async def handler(request: aiohttp.web.Request): model = request.app['model'] @@ -116,7 +117,7 @@ if __name__ == '__main__': app.add_routes(routes) data = {} - filename = "tehsession.json" + filename = "scoreboard.json" if os.path.isfile(filename): with open(filename) as f: data = json.load(f) diff --git a/model.py b/model.py index a3ab6f8..87fac0a 100644 --- a/model.py +++ b/model.py @@ -3,7 +3,7 @@ import base64 import json import os import datetime - +import hashlib def generate_random_id(_s=set()): while (new_id := base64.b32encode(bytearray(random.randint(0, 0xFF) for _ in range(10)))[:16].decode().lower()) in _s: @@ -11,6 +11,10 @@ def generate_random_id(_s=set()): _s.add(new_id) return new_id +if not "WASCHMARKENSECRET" in os.environ: + print("Please set the environment variable WASCHMARKENSECRET first") + exit() +SECRET = os.environ["WASCHMARKENSECRET"] class Model(object): class ApiMethod: @@ -22,362 +26,117 @@ class Model(object): def __contains__(self, item): return item in self.dict - def __init__(self, model = {}): - if "sessions" not in model: - model["sessions"] = [] - if "clients" not in model: - model["clients"] = [] - if "items" not in model: - model["items"] = [] - self.sockets = {} # mapping: clientid -> sockets - self.sessions = { session: Session(model = model["sessions"][session]) for session in model["sessions"] } - self.clients = { client: Client(model = model["clients"][client]) for client in model["clients"] } - self.items = { item: Item(model = model["items"][item]) for item in model["items"] } - self.filename = "tehsession.json" + def __init__(self, model = dict()): + self.filename = "scoreboard.json" + if "users" not in model: + model["users"] = list() + print(f"loaded model: {model}") + self.users = { User(modelstring).uuid : User(modelstring) for modelstring in model["users"]} + + #for _ in range(5): + # newuser = User() + # self.users[newuser.uuid] = newuser + + self.secretlookup = { self.users[uuid].get_secret() : uuid for uuid in self.users } + print(self.secretlookup) + #print(self.users) def to_json(self): model = { - "users": {session: self.sessions[session].to_json() for session in self.sessions }, - "clients": {client: self.clients[client].to_json() for client in self.clients }, - "items": {item: self.items[item].to_json() for item in self.items }, + "users": [self.users[user].to_json() for user in self.users ], } return model - @ApiMethod - async def get_public_model(self): - publicmodel = { - "users" : [ - { - "score": 50, - "maxscore": 75, - "timeout": 0, - "uuid": "wasisseneuuid?", - "name": "Knödelkind" - }, - { - "score": 58, - "maxscore": 75, - "timeout": 1595019787, - "uuid": "neuuidsolleindeutigsein", - "name": "Dominickque" - }, - { - "score": 69, - "maxscore": 100, - "timeout": 1595024761, - "uuid": "moneyboyswag420", - "name": "Andi" - }, - ] - } - return publicmodel + def verify_user(self, authtoken): + if authtoken in self.secretlookup: + return self.users[self.secretlookup[authtoken]] + raise Exception(f"Unauthorized user: {authtoken}") - @ApiMethod - async def test_api(self, clientid): - print(f'test_api {clientid=}') - - @ApiMethod - async def test_yeet(self, clientid): - raise Exception('yeet') - - @ApiMethod - async def change_username(self, clientid, username) -> str: - self.clients[clientid].name = username - - @ApiMethod - async def create_session(self, clientid, sessionname) -> str: - if not sessionname: - raise Exception(f"Sessionname cant be empty!") - session = Session(name = sessionname, owner = clientid) - self.sessions[session.id] = session - print("create_session was called") - - @ApiMethod - async def change_sessionname(self, clientid, sessionid, sessionname) -> str: - if not sessionname: - raise Exception("Cant be empty!") - session = self.sessions[sessionid] - if not (session.owner == clientid): - raise Exception("Ownly owner can change sessionname") - session.name = sessionname - - @ApiMethod - async def create_item(self, clientid, name, description, image): - client = self.clients[clientid] - if not client.session: - raise Exception("create_item requires the client to be host. But client is in no session") - session = self.sessions[client.session] - if (session.owner != client.id): - raise Exception("create_item requires the client to be host. But client is not the owner") - if not name: - raise Exception("create_item requires an item name") - item = Item(name = name, description = description, image = image) - self.items[item.id] = item - - - @ApiMethod - async def leave_session(self, clientid): - client = self.clients[clientid] - sessionid = client.session - if sessionid: - session = self.sessions[sessionid] - session.clients.remove(clientid) - self.clients[clientid].session = "" - - @ApiMethod - async def join_session(self, clientid, sessionid): - client = self.clients[clientid] - old_sessionid = client.session - # leave old session - if old_sessionid: - old_session = self.sessions[old_sessionid] - old_session.clients.remove(clientid) - session = self.sessions[sessionid] - client.session = sessionid - session.clients.append(client.id) - - - @ApiMethod - async def move_item(self, clientid, fromplayer, toplayer, itemid, toslot, fromslot): - client = self.clients[clientid] - fromclient = next((client for client in self.clients.values() if client.name == fromplayer), None) - toclient = next((client for client in self.clients.values() if client.name == toplayer), None) - if fromplayer == "master": - # create item - if not toclient: - raise Exception("to-client is illegal") - session = self.sessions[client.session] - if not session: - raise Exception("move item must be used in session") - if client.id != session.owner: - raise Exception("Only owner can move items") - toslot -= 1 - if toslot > len(session.inventories[toclient.id]): - raise Exception("Index out of toplayers range") - session.inventories[toclient.id].insert(toslot, itemid) - elif toplayer == "master": - if not fromclient: - raise Exception("from-client is illegal") - session = self.sessions[client.session] - if not session: - raise Exception("move item must be used in session") - if client.id != session.owner: - raise Exception("Only owner can move items") - if itemid not in session.inventories[fromclient.id]: - raise Exception("he does not have that item") - fromslot -= 1 - if fromslot > len(session.inventories[fromclient.id]): - raise Exception("Index out of fromplayer range") - session.inventories[fromclient.id].pop(fromslot) - else: - if not fromclient and not toclient: - raise Exception("from- or to-client are illegal") - session = self.sessions[client.session] - if not session: - raise Exception("move item must be used in session") - if client.id != session.owner: - raise Exception("Only owner can move items") - if itemid not in session.inventories[fromclient.id]: - raise Exception("he does not have that item") - toslot -= 1 - if toslot > len(session.inventories[toclient.id]): - raise Exception("Index out of toplayers range") - fromslot -= 1 - if fromslot > len(session.inventories[fromclient.id]): - raise Exception("Index out of fromplayer range") - session.inventories[fromclient.id].pop(fromslot) - session.inventories[toclient.id].insert(toslot, itemid) - - - - @ApiMethod - async def draw(self, clientid): - await self.send_state(clientid) - - async def send_lobby_view(self, clientid): - data = {} - client = self.clients[clientid] - - data["view"] = "lobby" - data["username"] = client.name - data["sessions"] = { - session.id: { - "id": session.id, - "name": session.name, - "owned": session.owner == client.id, - } for session in self.sessions.values() - } - for socket in self.sockets[clientid]: - await socket.send_json(data) - - async def send_master_view(self, clientid): - data = {} - client = self.clients[clientid] - session = self.sessions[client.session] - - data["view"] = "master" - data["username"] = client.name - data["session"] = session.to_json() - data["items"] = self.to_json()["items"] - data["inventories"] = {} - for _client in session.inventories: - _client = self.clients[_client] - inventory = session.get_items(_client.id) - inventory = list(map(lambda itemid: self.to_json()["items"][itemid], inventory)) - data["inventories"][_client.name] = inventory - - for socket in self.sockets[clientid]: - await socket.send_json(data) - - - async def send_session_view(self, clientid): - data = {} - client = self.clients[clientid] - session = self.sessions[client.session] - - data["view"] = "session" - data["username"] = client.name - data["session"] = session.name - data["inventories"] = {} - for _client in session.inventories: - _client = self.clients[_client] - inventory = session.get_items(_client.id) - inventory = list(map(lambda itemid: self.to_json()["items"][itemid], inventory)) - data["inventories"][_client.name] = inventory - - for socket in self.sockets[clientid]: - await socket.send_json(data) - - async def send_state(self, clientid): - # TODO: compute state, send to client - data = {} - client = self.clients[clientid] - session = self.sessions[client.session] if client.session else None - if session: - if session.owner == client.id: - await self.send_master_view(clientid) - else: - await self.send_session_view(clientid) - else: - await self.send_lobby_view(clientid) + def verify_admin(self, authtoken): + if authtoken == hashlib.sha256(SECRET.encode() + b"admintoken").hexdigest(): + return + raise Exception(f"Unauthorized admin: {authtoken}") def save(self): with open(self.filename, "w") as f: - json.dump(self.to_json(), f) + json.dump(self.to_json(), f, indent=2) - def exists_client(self, clientid: str) -> bool: - return clientid in self.clients + # + # Public API Methods + # - def create_client(self, name="Joe") -> str: - if not name: - raise Exception("Username cannot be empty!") - client = Client() - client.name = name - self.clients[client.id] = client - return client.id + @ApiMethod + async def get_public_model(self): + return self.to_json() - async def subscribe(self, clientid, socket): - if not clientid in self.sockets: - self.sockets[clientid] = [] - self.sockets[clientid].append(socket) - await self.send_state(clientid) + # + # Authorized API Methods + # - def unsubscribe(self, socket): - for client in self.sockets: - if socket in self.sockets[client]: - self.sockets[client].remove(socket) + @ApiMethod + async def get_user(self, authtoken): + user = self.verify_user(authtoken) + return {"user":user.name} -class Session: + @ApiMethod + async def set_score(self, authtoken, newscore): + user = self.verify_user(authtoken) + if newscore <= user.maxscore: + user.score = newscore + else: + raise Exception("Tried to raise user score above maxscore") - def __init__(self, model = None, owner = None, name = None): + # + # Admin API Methods + # + + @ApiMethod + async def add_user(self, authtoken, username): + self.verify_admin(authtoken) + newuser = User(username = username) + self.users[newuser.uuid] = newuser + + @ApiMethod + async def rename_user(self, authtoken, uuid, newusername): + self.verify_admin(authtoken) + if uuid in self.users: + self.users[uuid].name = newusername + else: + raise Exception("No such user") + + @ApiMethod + async def set_maxscore(self, authtoken, newmaxscore): + self.verify_admin(authtoken) + if newscore <= user.maxscore: + user.score = newscore + else: + raise Exception("Tried to raise user score above maxscore") + +class User: + + def __init__(self, username = "Default Username", model = None): if model: - self.id = model["id"] + self.uuid = model["uuid"] self.name = model["name"] - self.clients = model["clients"] - self.owner = model["owner"] - if "inventories" in model: - self.inventories = model["inventories"] - else: - self.inventories = {} - elif owner and name: - self.id = generate_random_id() - self.clients = [] - self.owner = owner - self.name = name - self.inventories = {} + self.score = model["score"] + self.maxscore = model["maxscore"] + self.timeout = model["timeout"] else: - raise Exception("Illegal session constructor") + self.uuid = generate_random_id() + self.name = username + self.score = 0 + self.maxscore = 0 + self.timeout = 0 - def get_items(self, playerid): - if playerid in self.inventories: - return self.inventories[playerid] - else: - return [] - - def give_item(self, playerid, itemid): - if not playerid in self.inventories: - self.inventories[playerid] = [] - self.inventories[playerid].append(itemid) + def get_secret(self): + return hashlib.sha256(SECRET.encode() + str(self.uuid).encode()).hexdigest() def to_json(self): model = { - "id": self.id, + "uuid": self.uuid, "name": self.name, - "clients": self.clients, - "owner": self.owner, - "inventories": self.inventories - } - return model - - - -class Client: - - def __init__(self, model = None): - if model: - self.id = model["id"] - self.name = model["name"] - self.session = model["session"] - else: - self.id = generate_random_id() - self.name = "Default Client Name" - self.session = "" - - def to_json(self): - model = { - "id": self.id, - "name": self.name, - "session": self.session, - } - return model - -class Item: - - def __init__(self, model = None, name = None, description = "", image = "", tags = {}): - if model: - self.id = model["id"] - self.name = model["name"] - self.description = model["description"] - self.image = model["image"] - if "tags" in model: - self.tags = model["tags"] - else: - self.tags = {} - elif name: - self.id = generate_random_id() - self.name = name - self.description = description - self.image = image - self.tags = tags - else: - raise Exception("Illegal Item Constructor") - - def to_json(self): - model = { - "id": self.id, - "name": self.name, - "description": self.description, - "image": self.image, - "tags": self.tags, + "score": self.score, + "maxscore": self.maxscore, + "timeout": self.timeout, } return model diff --git a/scoreboard.json b/scoreboard.json new file mode 100644 index 0000000..921b341 --- /dev/null +++ b/scoreboard.json @@ -0,0 +1,99 @@ +{ + "users": [ + { + "uuid": "bnqkcmu3vd3fq5fu", + "name": { + "uuid": "t55sfdftsozzy3mi", + "name": { + "uuid": "sprnnogu3cvzzsqf", + "name": "Default User Name", + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + { + "uuid": "sywskga5s3m6wxuq", + "name": { + "uuid": "zq2tratnuyfbfrx2", + "name": { + "uuid": "c5lrovzryckbxgwg", + "name": "Default User Name", + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + { + "uuid": "ytpvp4dopx54gfld", + "name": { + "uuid": "snmtkzofojtqma3a", + "name": { + "uuid": "wxl42rw4zdghjgve", + "name": "Default User Name", + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + { + "uuid": "dxkahhq65uo63xqp", + "name": { + "uuid": "6u5z6po6gtmh52fr", + "name": { + "uuid": "shnisaos4vxnfcvq", + "name": "Default User Name", + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + { + "uuid": "3uw4wif2heales72", + "name": { + "uuid": "fg7kxyv36ls6a6w5", + "name": { + "uuid": "72onbiw4phvtd54y", + "name": "Default User Name", + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + }, + "score": 0, + "maxscore": 0, + "timeout": 0 + } + ] +} \ No newline at end of file diff --git a/tehsession.json b/tehsession.json deleted file mode 100644 index a1ee7bc..0000000 --- a/tehsession.json +++ /dev/null @@ -1 +0,0 @@ -{"users": {}, "clients": {}, "items": {}} \ No newline at end of file diff --git a/waschmarkensecret b/waschmarkensecret new file mode 100644 index 0000000..7dbfa13 --- /dev/null +++ b/waschmarkensecret @@ -0,0 +1 @@ +swiggityswootyiamcomingforthatwashingcoin