├── .DS_Store
├── .gitignore
├── .idea
├── FastApiWebSocket.iml
├── inspectionProfiles
│ └── profiles_settings.xml
├── misc.xml
├── vcs.xml
└── workspace.xml
├── Demo
├── .DS_Store
└── FastApi Websocket Demo.mp4
├── ReadMe.md
├── connection_manager.py
├── main.py
├── models
└── register_to_room_model.py
├── requirements.txt
└── static
├── index.html
└── script.js
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeterAkande/WebSocketChat-FastAPI/9c51287a425f9ab9e3253afbba8d96afc95bdee7/.DS_Store
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | venv/
2 | __pycache__
3 |
--------------------------------------------------------------------------------
/.idea/FastApiWebSocket.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/workspace.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {
32 | "associatedIndex": 6
33 | }
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {
43 | "keyToString": {
44 | "ASKED_ADD_EXTERNAL_FILES": "true",
45 | "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
46 | "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
47 | "DefaultHtmlFileTemplate": "HTML File",
48 | "RunOnceActivity.OpenProjectViewOnStart": "true",
49 | "RunOnceActivity.ShowReadmeOnStart": "true",
50 | "SHARE_PROJECT_CONFIGURATION_FILES": "true",
51 | "git-widget-placeholder": "main",
52 | "last_opened_file_path": "/Users/akandepeter/DevProjects/Backend/personal/FastApiWebSocket",
53 | "settings.editor.selected.configurable": "preferences.pluginManager"
54 | }
55 | }
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | 1696926283686
84 |
85 |
86 | 1696926283686
87 |
88 |
89 |
90 | 1696997060890
91 |
92 |
93 |
94 | 1696997060890
95 |
96 |
97 |
98 | 1696997453700
99 |
100 |
101 |
102 | 1696997453700
103 |
104 |
105 |
106 |
107 |
108 |
109 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
--------------------------------------------------------------------------------
/Demo/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeterAkande/WebSocketChat-FastAPI/9c51287a425f9ab9e3253afbba8d96afc95bdee7/Demo/.DS_Store
--------------------------------------------------------------------------------
/Demo/FastApi Websocket Demo.mp4:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PeterAkande/WebSocketChat-FastAPI/9c51287a425f9ab9e3253afbba8d96afc95bdee7/Demo/FastApi Websocket Demo.mp4
--------------------------------------------------------------------------------
/ReadMe.md:
--------------------------------------------------------------------------------
1 | ## Chat App WebSocket Implementation
2 |
3 | A FastAPI Backend Service that simulates the Websocket needs of a chat app that involves:
4 | - Users being able to Exchange messages with one another
5 | - The concept of rooms or group chats
6 |
7 | This uses an in memory List to keep track of Websocket connections.
8 | To check the version that uses redis PUB/SUB, check the [redis_db_branch](https://github.com/PeterAkande/WebSocketChat-FastAPI/tree/redis_backend)
9 |
10 |
11 |
12 | #### Demo
13 |
14 | https://github.com/PeterAkande/WebSocketChat-FastAPI/assets/64542587/00824b78-3092-4fd1-b7d1-5fefa5e56e27
15 |
16 | #### View Demo On Youtube Instead
17 | [](https://youtu.be/G-YG04E1CuY)
18 |
--------------------------------------------------------------------------------
/connection_manager.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | from typing import Dict, Set, List, Tuple
3 | from operator import itemgetter
4 |
5 | from fastapi import WebSocket
6 | from starlette.websockets import WebSocketState
7 |
8 |
9 | class ConnectionManager:
10 | """
11 | This would handle all Message broadcast and all connections to the Service.
12 | """
13 |
14 | def __init__(self):
15 | """
16 | self.connections would be an in memory db of the users connected to a DB.
17 | It would function with an forward index like structure.
18 |
19 | Something like:
20 | {
21 | room_id: {user1, user2, user3},
22 | room_id_1: {user1, user3}
23 | }
24 | """
25 | self.connections: Dict[str, Set[str]] = {} # The Room and the set of users connected
26 | self.user_connections: Dict[str, WebSocket] = {} # the user connections
27 |
28 | async def save_user_connection_record(self, ws_connection: WebSocket, user_id: str):
29 | """
30 | This would save a user record to the Websocket connections that the connection manager is currently keeping
31 | """
32 |
33 | self.user_connections[user_id] = ws_connection
34 | await self.send_message_to_ws_connection(message="Connection successful", ws_connection=ws_connection)
35 |
36 | async def add_user_connection_to_room(self, room_id: str, user_id: str) -> (bool, str):
37 | """
38 | This function would add a user to a Room
39 | This user id would be added to the ids of the users that are listening to a room
40 | """
41 |
42 | # Try to get the user details from the dictionary of connections
43 | # It's a HashMap, so the Time Complexity is O(1)
44 |
45 | user_ws_connection = self.user_connections.get(user_id, None)
46 |
47 | if user_ws_connection is None:
48 | return False, "User Not Found"
49 |
50 | # The user connection was gotten.
51 |
52 | is_connection_active = await self.check_if_ws_connection_is_still_active(user_ws_connection)
53 |
54 | if not is_connection_active:
55 | self.user_connections.pop(user_id)
56 | return False, "Connection not Active"
57 |
58 | # Check if the room ID exists. If it does not, create one.
59 | if room_id not in self.connections.keys():
60 | self.connections[room_id] = {user_id}
61 | else:
62 | self.connections[room_id].add(user_id)
63 |
64 | return True, "Connection Successful"
65 |
66 | async def check_if_ws_connection_is_still_active(self, ws_connection: WebSocket, message=".") -> bool:
67 | """
68 | This function would check if the connection is still active. It tries to send a message
69 | """
70 |
71 | if not (
72 | ws_connection.application_state == WebSocketState.CONNECTED and ws_connection.client_state == WebSocketState.CONNECTED):
73 | return False
74 |
75 | # Try to send a message
76 | try:
77 | await ws_connection.send_text(message)
78 | except RuntimeError:
79 | return False
80 |
81 | except Exception as e:
82 | traceback.print_exc()
83 | return False
84 |
85 | return True
86 |
87 | async def send_message_to_room(self, room_id: str, message: str):
88 | """
89 | Messages in this Program are Texts.
90 | """
91 |
92 | room_connections = self.connections.get(room_id, {})
93 | if len(room_connections) == 0:
94 | return
95 | users_ws_connections = itemgetter(*room_connections)(self.user_connections)
96 |
97 | print(users_ws_connections)
98 | if type(users_ws_connections) is not tuple:
99 | users_ws_connections = [users_ws_connections]
100 |
101 | for connection in users_ws_connections:
102 | is_sent, sent_message_response_info = await self.send_message_to_ws_connection(
103 | message=f"Room {room_id} --> {message}", ws_connection=connection)
104 |
105 | # It can be chosen to remove the connection from the self.user_connections if is_sent is False.
106 |
107 | async def send_message_to_ws_connection(self, message: str, ws_connection: WebSocket) -> (bool, str):
108 | try:
109 | await ws_connection.send_text(message)
110 | return True, "Message sent!"
111 | except RuntimeError:
112 | return False, "Message Not Sent, Websocket is disconnected"
113 |
114 | except Exception as e:
115 | traceback.print_exc()
116 |
117 | return False, "Error Sending Message"
118 |
119 | def remove_user_connection(self, user_id):
120 | self.user_connections.pop(user_id)
121 |
--------------------------------------------------------------------------------
/main.py:
--------------------------------------------------------------------------------
1 | import traceback
2 | from typing import Dict, Set
3 |
4 | from fastapi import FastAPI, WebSocket, WebSocketException, WebSocketDisconnect
5 | from fastapi.responses import HTMLResponse, FileResponse
6 | from fastapi.exceptions import HTTPException
7 |
8 | from connection_manager import ConnectionManager
9 | from models.register_to_room_model import RegisterToRoom
10 |
11 | number_of_socket_connections = 0
12 | connection_manager = ConnectionManager()
13 |
14 | app = FastAPI()
15 |
16 |
17 | @app.get("/")
18 | async def get():
19 | return FileResponse("static/index.html")
20 |
21 |
22 | @app.post("/register_to_room/")
23 | async def register_user_to_room(body: RegisterToRoom):
24 | """
25 | This route would register a user to a route
26 | """
27 |
28 | is_added, message = await connection_manager.add_user_connection_to_room(user_id=body.user_id, room_id=body.room_id)
29 |
30 | print(connection_manager.user_connections)
31 | print(connection_manager.connections)
32 | # Do whatever you like with is_added later
33 |
34 | if not is_added:
35 | raise HTTPException(detail={"message": message}, status_code=400)
36 |
37 | return {
38 | "message": message
39 | }
40 |
41 |
42 | @app.websocket("/ws/{user_id}")
43 | async def websocket_endpoint(user_id: str, websocket: WebSocket):
44 | await websocket.accept()
45 | global number_of_socket_connections
46 |
47 | # Add the user to the connection stack
48 | await connection_manager.save_user_connection_record(user_id=user_id, ws_connection=websocket)
49 | try:
50 | number_of_socket_connections += 1
51 | while True:
52 | data = await websocket.receive_json()
53 |
54 | room_id = data["room_id"]
55 | message = data["message"]
56 |
57 | await connection_manager.send_message_to_room(message=message, room_id=room_id)
58 | except WebSocketDisconnect:
59 | # Remove the user from the connection stack
60 | connection_manager.remove_user_connection(user_id=user_id)
61 |
62 | except WebSocketException as e:
63 | traceback.print_exc()
64 |
65 | print("An error occurred and i dont know the details")
66 |
--------------------------------------------------------------------------------
/models/register_to_room_model.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 |
4 | class RegisterToRoom(BaseModel):
5 | user_id: str
6 | room_id: str
7 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | annotated-types==0.6.0
2 | anyio==3.7.1
3 | click==8.1.7
4 | exceptiongroup==1.1.3
5 | fastapi==0.103.2
6 | h11==0.14.0
7 | httptools==0.6.0
8 | idna==3.4
9 | pydantic==2.4.2
10 | pydantic-core==2.10.1
11 | python-dotenv==1.0.0
12 | PyYAML==6.0.1
13 | sniffio==1.3.0
14 | starlette==0.27.0
15 | typing-extensions==4.8.0
16 | uvicorn==0.23.2
17 | uvloop==0.17.0
18 | watchfiles==0.20.0
19 | websockets==11.0.3
20 |
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Chat
5 |
6 |
7 | WebSocket Chat
8 |
33 |
35 |
157 |
158 |
--------------------------------------------------------------------------------
/static/script.js:
--------------------------------------------------------------------------------
1 | var ws = null
2 | var userId = null
3 |
4 | function connectToWS(event) {
5 |
6 | var userIdField = document.getElementById("userIdText")
7 | var connectionInfoSpan = document.getElementById("connectionInfo")
8 |
9 | if (userIdField.value.length == 0) {
10 | // The String is empty
11 | connectionInfoSpan.innerHTML = "Please input User Unique id"
12 | connectionInfoSpan.style.color = "red"
13 |
14 | event.preventDefault()
15 | return;
16 | }
17 |
18 | userId = userIdField.value
19 | connectionInfoSpan.innerHTML = "Connection Made Successfully!"
20 | connectionInfoSpan.style.color = "green"
21 |
22 | //The user Id was gotten
23 | //Initiate the connection
24 | let wsUrl = "ws://localhost:8000/ws/" + userId
25 | ws = new WebSocket(wsUrl);
26 |
27 | ws.onmessage = function (event) {
28 | var messages = document.getElementById('messages')
29 | var message = document.createElement('li')
30 | var content = document.createTextNode(event.data)
31 | message.appendChild(content)
32 | messages.appendChild(message)
33 | };
34 | event.preventDefault()
35 | }
36 |
37 | async function listenToRoom(event) {
38 | var roomIdField = document.getElementById("connectRoomIdText")
39 | var connectRoomInfoSpan = document.getElementById("connectRoomInfo")
40 |
41 | if (roomIdField.value.length == 0) {
42 | connectRoomInfoSpan.innerHTML = "Please Enter the room Id"
43 | event.preventDefault()
44 | return;
45 | }
46 |
47 |
48 | if (userId == null) {
49 | connectRoomInfoSpan.innerHTML = "User Id not Set"
50 | event.preventDefault()
51 | return;
52 | }
53 |
54 | //The whole data has now been gotten, Make the request now
55 |
56 | response = await fetch("/register_to_room", {
57 | method: "POST",
58 | body: JSON.stringify({
59 | user_id: userId,
60 | room_id: roomIdField.value,
61 | }),
62 | headers: {
63 | "Content-type": "application/json; charset=UTF-8"
64 | }
65 | }
66 | )
67 |
68 | if (!response.ok) {
69 | connectRoomInfoSpan.innerHTML = "Error Joining Room"
70 | event.preventDefault()
71 | return;
72 | }
73 |
74 | res = await response.json()
75 |
76 | connectRoomInfoSpan.innerHTML = "Room Joined Successfully!"
77 | connectRoomInfoSpan.style.color = "green"
78 | event.preventDefault()
79 |
80 |
81 | }
82 | function sendMessage(event) {
83 | var input = document.getElementById("messageText")
84 | var roomId = document.getElementById("roomIDMessageText")
85 | var sendMessageInfo = document.getElementById("sendMessageInfo")
86 |
87 |
88 | if (roomId.value.length == 0) {
89 | sendMessageInfo.innerHTML = "Please Enter the room Id"
90 | event.preventDefault()
91 | return false;
92 | }
93 |
94 | if (input.value.length == 0) {
95 | sendMessageInfo.innerHTML = "Please enter a valid Message"
96 | event.preventDefault()
97 | return false;
98 | }
99 |
100 |
101 | if (ws == null) {
102 | sendMessageInfo.innerHTML = "No Connection Made"
103 | event.preventDefault()
104 | return false;
105 | }
106 |
107 | var message = {
108 | room_id: roomId.value,
109 | message: input.value,
110 | }
111 |
112 | ws.send(JSON.stringify(message))
113 |
114 | input.value = ''
115 | sendMessageInfo.innerHTML = ""
116 | event.preventDefault()
117 | }
118 |
--------------------------------------------------------------------------------