From bfe473021ae7af5ac0629b1a1015abc90a7f01b2 Mon Sep 17 00:00:00 2001 From: Tom Vaughan Date: Sun, 18 Jun 2023 19:46:00 +0100 Subject: [PATCH] Attempt to refactor socket.io server to better integrate with FastAPI server to fix CORS issues --- fight_me_backend/main.py | 136 +--------------------------- fight_me_backend/socket_handlers.py | 129 ++++++++++++++++++++++++++ poetry.lock | 21 ++++- pyproject.toml | 1 + 4 files changed, 155 insertions(+), 132 deletions(-) create mode 100644 fight_me_backend/socket_handlers.py diff --git a/fight_me_backend/main.py b/fight_me_backend/main.py index f4f2bb5..40afe55 100644 --- a/fight_me_backend/main.py +++ b/fight_me_backend/main.py @@ -1,15 +1,12 @@ #!/usr/bin/env python -import random -import time + import uvicorn import socketio -import asyncio -import uuid + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from threading import Thread -from collections import deque +from fastapi_socketio import SocketManager app = FastAPI() app.add_middleware( @@ -19,131 +16,8 @@ allow_headers=["*"], ) -sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") -sio_app = socketio.ASGIApp(sio, other_asgi_app=app) - - -users = {} -messages = {} -unfilled_rooms = deque() - - -@app.get("/health") -async def health_check(): - return {"status": "ok"} - - -def broadcast_message(): - message = "Hello, clients!" - socketio.emit("broadcast_message", message, broadcast=True) - - -@sio.on("connect") -def test_connect(sid, data): - print(f"⚡: {sid} user just connected!") - users[sid] = {"room": None} - print(users) - - -@sio.on("disconnect") -async def test_disconnect(sid): - print(f"🔥: {sid} user disconnected") - sio.leave_room(sid, users[sid]["room"]) - users.pop(sid) - await sio.disconnect(sid) - print(users) - - -@sio.event -async def message(sid, data): - print(f"SID: {sid}") - print(data) - - data["timestamp"] = time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time())) - - print(f"Message: {data['text']} from {data['name']} at {data['timestamp']}") - - if data.get("room") in messages: - messages[data["room"]].append(data) - else: - messages[data["room"]] = [data] - - print(messages) - - await sio.emit("messageResponse", data) - - -@sio.event -async def getMessages(sid, data): - print(f"SID {sid} catching up on messages for room {data['room']}") - - print(f"Message history: {messages.get(data['room'])}") - await sio.emit("getMessagesResponse", messages.get(data["room"])) - - -def getRoomToJoin(): - if len(unfilled_rooms): - return unfilled_rooms.popleft() - else: - room = uuid.uuid4().hex - unfilled_rooms.append(room) - return room - - -@sio.event -async def getRoom(sid): - room = getRoomToJoin() - print(f"Adding SID {sid} to room {room}") - sio.enter_room(sid, room) - users[sid]["room"] = room - - topic = "Lorem Ipsum" - side = bool(random.getrandbits(1)) - - await sio.emit("getRoomResponse", {"room": room, "topic": topic, "side": side}) - - -@sio.event -async def leaveRoom(sid, data): - print(f"Removing SID {sid} from room {data['room']}") - sio.leave_room(sid, data["room"]) - - num_in_room = sum(1 for _, user_data in users.items() if user_data.get("room") is data["room"]) - print(f"Num left in room: {num_in_room}") - if num_in_room > 0: - unfilled_rooms.append(data["room"]) - - users[sid]["room"] = None - await sio.emit("leaveRoomResponse", {"room": data["room"]}, to=sid) - - -async def connected_users(): - users_in_rooms = sum(1 for _, user_data in users.items() if user_data.get("room") is not None) - await sio.emit("connectedUsers", {"users": len(users), "usersInRooms": users_in_rooms}) - - -async def connected_users_timer(interval_seconds): - while True: - await connected_users() - await asyncio.sleep(interval_seconds) - - -def start_connected_users_timer(): - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - try: - loop.create_task(connected_users_timer(2)) - loop.run_forever() - except KeyboardInterrupt: - loop.stop() - finally: - loop.close() - - -thread = Thread(target=start_connected_users_timer, daemon=True) -thread.start() +socket_manager = SocketManager(app=app) if __name__ == "__main__": - uvicorn.run(sio_app, host="0.0.0.0", port=5000) + uvicorn.run(app, host="0.0.0.0", port=5000) diff --git a/fight_me_backend/socket_handlers.py b/fight_me_backend/socket_handlers.py new file mode 100644 index 0000000..063e14f --- /dev/null +++ b/fight_me_backend/socket_handlers.py @@ -0,0 +1,129 @@ +import random +import time +import asyncio +import uuid +from threading import Thread +from collections import deque + +from .main import app, socket_manager as sm + +users = {} +messages = {} +unfilled_rooms = deque() + + +@app.get("/health") +async def health_check(): + return {"status": "ok"} + + +def broadcast_message(): + message = "Hello, clients!" + sm.emit("broadcast_message", message, broadcast=True) + + +@app.sio.on("connect") +def test_connect(sid, data): + print(f"⚡: {sid} user just connected!") + users[sid] = {"room": None} + print(users) + + +@app.sio.on("disconnect") +async def test_disconnect(sid): + print(f"🔥: {sid} user disconnected") + app.sio.leave_room(sid, users[sid]["room"]) + users.pop(sid) + await app.sio.disconnect(sid) + print(users) + + +@app.sio.event +async def message(sid, data): + print(f"SID: {sid}") + print(data) + + data["timestamp"] = time.strftime("%Y-%m-%d %H:%M", time.localtime(time.time())) + + print(f"Message: {data['text']} from {data['name']} at {data['timestamp']}") + + if data.get("room") in messages: + messages[data["room"]].append(data) + else: + messages[data["room"]] = [data] + + print(messages) + + await app.sio.emit("messageResponse", data) + + +@app.sio.event +async def getMessages(sid, data): + print(f"SID {sid} catching up on messages for room {data['room']}") + + print(f"Message history: {messages.get(data['room'])}") + await app.sio.emit("getMessagesResponse", messages.get(data["room"])) + + +def getRoomToJoin(): + if len(unfilled_rooms): + return unfilled_rooms.popleft() + else: + room = uuid.uuid4().hex + unfilled_rooms.append(room) + return room + + +@app.sio.event +async def getRoom(sid): + room = getRoomToJoin() + print(f"Adding SID {sid} to room {room}") + app.sio.enter_room(sid, room) + users[sid]["room"] = room + + topic = "Lorem Ipsum" + side = bool(random.getrandbits(1)) + + await app.sio.emit("getRoomResponse", {"room": room, "topic": topic, "side": side}) + + +@app.sio.event +async def leaveRoom(sid, data): + print(f"Removing SID {sid} from room {data['room']}") + app.sio.leave_room(sid, data["room"]) + + num_in_room = sum(1 for _, user_data in users.items() if user_data.get("room") is data["room"]) + print(f"Num left in room: {num_in_room}") + if num_in_room > 0: + unfilled_rooms.append(data["room"]) + + users[sid]["room"] = None + await app.sio.emit("leaveRoomResponse", {"room": data["room"]}, to=sid) + + +async def connected_users(): + users_in_rooms = sum(1 for _, user_data in users.items() if user_data.get("room") is not None) + await app.sio.emit("connectedUsers", {"users": len(users), "usersInRooms": users_in_rooms}) + + +async def connected_users_timer(interval_seconds): + while True: + await connected_users() + await asyncio.sleep(interval_seconds) + + +def start_connected_users_timer(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + try: + loop.create_task(connected_users_timer(2)) + loop.run_forever() + except KeyboardInterrupt: + loop.stop() + finally: + loop.close() + + +thread = Thread(target=start_connected_users_timer, daemon=True) +thread.start() diff --git a/poetry.lock b/poetry.lock index 49c973c..0dc36c3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -159,6 +159,25 @@ dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (> doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +[[package]] +name = "fastapi-socketio" +version = "0.0.10" +description = "Easily integrate socket.io with your FastAPI app." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "fastapi-socketio-0.0.10.tar.gz", hash = "sha256:202f9b319f010001cbd1114ec92a0d9eb5f5ca9316eae5fd41a6088da0812727"}, + {file = "fastapi_socketio-0.0.10-py3-none-any.whl", hash = "sha256:11c2bfa3f25d786bd860ed13c892472e86bfeba85e7a0bec4f922ae5e4d8650f"}, +] + +[package.dependencies] +fastapi = ">=0.61.1" +python-socketio = ">=4.6.0" + +[package.extras] +test = ["pytest"] + [[package]] name = "filelock" version = "3.12.0" @@ -777,4 +796,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "79ebe4ff69aa4e089e1b021fefd44376dbf28625e6bf83f4b77d92faebcf8fa1" +content-hash = "d0eef47dfa9315e46d592a20217933b9782c76aaef5ea52b8a0f12070e8f37be" diff --git a/pyproject.toml b/pyproject.toml index f0de9f0..5f33229 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,6 +11,7 @@ python = "^3.11" python-socketio = "^5.8.0" uvicorn = {extras = ["standard"], version = "^0.22.0"} fastapi = "^0.95.2" +fastapi-socketio = "^0.0.10" [tool.poetry.group.dev.dependencies]