From a5e323456826ff48ed2a889b7b82bc1d60d44eae Mon Sep 17 00:00:00 2001
From: Evgenij Titarenko <frundle@teasanctuary.ru>
Date: Mon, 10 Jul 2023 23:20:36 +0300
Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?=
 =?UTF-8?q?=D0=BA=D0=B0=20=D0=B2=D0=B7=D0=B0=D0=B8=D0=BC=D0=BE=D0=B4=D0=B5?=
 =?UTF-8?q?=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D1=8F=20=D1=81=20=D0=BF=D0=BE?=
 =?UTF-8?q?=D0=B4=D0=BA=D0=BB=D1=8E=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=D0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 game.py |  3 ++
 main.py | 90 ++++++++++++++++++++++++++++++++++++++++++---------------
 2 files changed, 70 insertions(+), 23 deletions(-)

diff --git a/game.py b/game.py
index 4ba0739..0f13702 100644
--- a/game.py
+++ b/game.py
@@ -222,3 +222,6 @@ class Room:
     def remove_user(self, user: User):
         if user in self.users:
             self.users.remove(user)
+
+    def is_empty(self):
+        return not len(self.users) > 0
diff --git a/main.py b/main.py
index c346b1a..38f50bc 100644
--- a/main.py
+++ b/main.py
@@ -17,8 +17,12 @@ import logging
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware
 from fastapi_socketio import SocketManager
+from cachetools import TTLCache
 from game import Room, User, load_streets
 from objects import ErrCode, UserCheck
+from threading import Timer
+
+ROOM_DISCONNECT_TIMEOUT = 60*5  # TODO: Сохранить остаток таймера для каждого пользователя
 
 logging.basicConfig(level=logging.DEBUG, format="%(levelname)s:\t%(asctime)s\t%(message)s")
 load_streets()
@@ -33,9 +37,10 @@ app.add_middleware(
     allow_headers=["*"],
 )
 sio = SocketManager(app=app, mount_location="/ws", socketio_path="game", cors_allowed_origins=[])
-# sio_sessions = TTLCache(maxsize=10000, ttl=24 * 60 * 60)
-rooms = []
-users = []  # TODO: implement as dict
+sio_sessions = TTLCache(maxsize=10000, ttl=24 * 60 * 60)
+rooms = dict()
+users = dict()
+users_to_disconnect = dict()
 
 
 # TODO: Time-based tokens
@@ -44,15 +49,14 @@ def check_user(username, password=None, token=None):
         return False
     else:
         global users
-        selected_users = tuple(filter(lambda usr: usr.username == username, users))
-        if len(selected_users) == 0:
+        if username not in users:
             if token:
                 logging.debug(f"Invalid token for user {username}")
                 return UserCheck.INVALID_CREDENTIALS
             logging.debug(f"Invalid user {username}")
             return UserCheck.USER_DOESNT_EXISTS
         else:
-            user = selected_users[0]
+            user = users[username]
             if user.token == token or user.password == password:
                 logging.debug(f"Valid credentials for user {username}")
                 return user
@@ -68,38 +72,47 @@ async def sio_create_room(sid, username, token, password):
             new_room = Room(password)
             logging.info(f"User {username} created room {new_room.room_id}")
             new_room.add_user(user)
-            rooms.append(new_room)
-            user.in_room = new_room.room_id  # TODO: not add user on creation
-            user.room_password = new_room.password
-            await sio_send_user_info(sid, user)
+            rooms[new_room.room_id] = new_room
+            await sio.emit("roomCreated", {"room_id": new_room.room_id, "room_password": new_room.password}, room=sid)
         case _:
             return False  # TODO: some errors
 
+
 async def sio_send_user_info(sid, user):
     userdata = user.get_dict()
     userdata["token"] = user.token
     if user.in_room:
         userdata["in_room"] = user.in_room
-        userdata["password"] = user.password
+        userdata["room_password"] = user.room_password
     await sio.emit("userInfo", userdata, room=sid)
 
 
 @sio.on("joinRoom")
-async def sio_join_room(sid, room_id, room_password):  # TODO: Check if user already in room
-    global rooms
-    selected_room = tuple(filter(lambda room: room.room_id == room_id, rooms))
-    if len(selected_room) == 0:
-        await sio_throw_error(sid, ErrCode.ROOM_DOESNT_EXIST)
-    else:
-        room = selected_room[0]
-        room_data = room.get_dict()
-        sio.enter_room(sid, room.room_id)
-        if room.password == room_password:
-            await sio.emit("roomInfo", room_data, room=sid)
+async def sio_join_room(sid, username, token, room_id, room_password):  # TODO: Check if user already in room
+    match check_user(username, token=token):
+        case User(username=username) as user:
+            global rooms
+            if room_id not in rooms:
+                logging.debug(f"Room {room_id} doesn't found")
+                await sio_throw_error(sid, ErrCode.ROOM_DOESNT_EXIST)
+            else:
+                room = rooms[room_id]
+                if room.password == room_password:
+                    user.in_room = room.room_id
+                    user.room_password = room.password
+                    room_data = room.get_dict()
+                    sio.enter_room(sid, room.room_id)
+                    await sio_send_user_info(sid, user)
+                    await sio.emit("roomInfo", room_data, room=sid)
+        case _:
+            await sio_throw_error(sid, ErrCode.INVALID_CREDENTIALS)
+            return False
 
 
 @sio.on("connect")
 async def sio_connect(sid, _, auth):
+    if sid in sio_sessions:
+        return False
     if not ("username" in auth and ("password" in auth or "token" in auth))\
             or not auth["username"].isalnum()\
             or len(auth["username"]) > 16:
@@ -110,12 +123,20 @@ async def sio_connect(sid, _, auth):
     password = auth.get("password", None)
     match check_user(username, password, token):
         case User(username=username) as user:
+            user.sid = sid
+            sio_sessions[sid] = user
+            if username in users_to_disconnect:
+                timer = users_to_disconnect[username]
+                timer.cancel()
+                del users_to_disconnect[username]
             await sio_send_user_info(sid, user)
         case UserCheck.USER_DOESNT_EXISTS:
             if not password:
                 return False
             user = User(username, password)
-            users.append(user)
+            users[username] = user
+            user.sid = sid
+            sio_sessions[sid] = user
             await sio_send_user_info(sid, user)
         case UserCheck.INVALID_CREDENTIALS:
             await sio_throw_error(sid, ErrCode.INVALID_CREDENTIALS)
@@ -132,7 +153,30 @@ async def sio_throw_error(sid, error):
 
 @sio.on("disconnect")
 async def sio_disconnect(sid):
+    user = sio_sessions[sid]
     logging.debug(f"{sid} disconnected")
+    if user.in_room:
+        logging.info(f"User {user.username} disconnected while in the room {user.in_room}")
+        global users_to_disconnect
+        t = Timer(ROOM_DISCONNECT_TIMEOUT, disconnect_user, args=[user])
+        users_to_disconnect[user.username] = t
+        t.start()
+
+def disconnect_user(user):
+    global rooms
+    room = rooms[user.in_room]
+    room.remove_user(user)
+    logging.info(f"User {user.username} disconnected from room {room.room_id}")
+    if room.is_empty():
+        logging.info(f"Room {room.room_id} is closed")
+        del rooms[room.room_id]
+        del room
+    t = users_to_disconnect[user.username]
+    t.cancel()
+    del t
+    del users_to_disconnect[user.username]
+
+
 
 
 @sio.on("rollDices")