Skip to content

Commit fad6c22

Browse files
committed
specify python version in CI
1 parent a770a1b commit fad6c22

File tree

8 files changed

+120
-30
lines changed

8 files changed

+120
-30
lines changed

Diff for: .github/workflows/ci.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ jobs:
1313
- name: Install Python
1414
uses: actions/setup-python@v5
1515
with:
16-
python-version: "3.11"
16+
python-version: "3.11.3"
1717
- name: Install dependencies
1818
run: |
19-
poetry env use "3.11"
19+
poetry env use "3.11.3"
2020
poetry export --only lint --output lint-requirements.txt
2121
pip install -r lint-requirements.txt
2222
- name: Run Ruff
@@ -49,13 +49,13 @@ jobs:
4949
- name: Install Python
5050
uses: actions/setup-python@v5
5151
with:
52-
python-version: "3.11"
52+
python-version: "3.11.3"
5353

5454
- uses: actions/setup-python@v4
5555
with:
56-
python-version: 3.11
56+
python-version: 3.11.3
5757
- run: |
58-
poetry env use "3.11"
58+
poetry env use "3.11.3"
5959
poetry install
6060
poetry run pytest -x -n auto --dist loadfile
6161
env:

Diff for: docker-compose.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ services:
1616
- db
1717
networks:
1818
- chat-net
19-
command: uvicorn src.main:app --host=0.0.0.0 --port=8001 --reload
19+
command: gunicorn -w 2 -k uvicorn.workers.UvicornWorker src.main:app -b 0.0.0.0:8001
2020

2121
db:
2222
image: postgres:15-alpine

Diff for: src/config.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class GlobalSettings(BaseSettings):
4040
ADMIN_SECRET_KEY: str = "Hv9LGqARc473ceBUYDw1FR0QaXOA3Ky4"
4141

4242
# redis for caching
43-
REDIS_CACHE_ENABLED: bool = True
43+
REDIS_CACHE_ENABLED: bool = False
4444
REDIS_HOST: str = "chat-redis"
4545
REDIS_PORT: str | int = 6379
4646
REDIS_PASSWORD: str | None = None

Diff for: src/managers/websocket_manager.py

+38-1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def __init__(self):
1515
self.chats: dict = {} # stores user WebSocket connections by chat {"chat_guid": {ws1, ws2}, ...}
1616
self.pubsub_client = RedisPubSubManager()
1717
self.user_guid_to_websocket: dict = {} # stores user_guid: {ws1, ws2} combinations
18+
self.user_channel: dict = {}
1819

1920
def handler(self, message_type):
2021
def decorator(func):
@@ -43,11 +44,33 @@ async def broadcast_to_chat(self, chat_guid: str, message: str | dict) -> None:
4344
message = json.dumps(message)
4445
await self.pubsub_client.publish(chat_guid, message)
4546

47+
async def create_user_channel(self, user_guid: str, websocket: WebSocket):
48+
if user_guid in self.user_channel:
49+
self.user_channel[user_guid].add(websocket)
50+
else:
51+
self.user_channel[user_guid] = {websocket}
52+
await self.pubsub_client.connect()
53+
pubsub_subscriber = await self.pubsub_client.subscribe(user_guid)
54+
asyncio.create_task(self._pubsub_data_reader_for_user(pubsub_subscriber))
55+
print("Guys", self.user_channel)
56+
57+
async def broadcast_to_user(self, user_guid: str, message: str | dict):
58+
if isinstance(message, dict):
59+
message = json.dumps(message)
60+
await self.pubsub_client.publish(user_guid, message)
61+
62+
async def remove_websocket_from_user_channel(self, user_guid: str, websocket: WebSocket) -> None:
63+
self.user_channel[user_guid].remove(websocket)
64+
if len(self.user_channel[user_guid]) == 0:
65+
del self.user_channel[user_guid]
66+
logger.info(f"Removing user from PubSub channel {user_guid}")
67+
await self.pubsub_client.unsubscribe(user_guid)
68+
4669
async def remove_user_from_chat(self, chat_guid: str, websocket: WebSocket) -> None:
4770
self.chats[chat_guid].remove(websocket)
4871
if len(self.chats[chat_guid]) == 0:
4972
del self.chats[chat_guid]
50-
logger.info("Removing user from PubSub channel {chat_guid}")
73+
logger.info(f"Removing user from PubSub channel {chat_guid}")
5174
await self.pubsub_client.unsubscribe(chat_guid)
5275

5376
async def remove_user_guid_to_websocket(self, user_guid: str, websocket: WebSocket):
@@ -69,5 +92,19 @@ async def _pubsub_data_reader(self, pubsub_subscriber):
6992
except Exception as exc:
7093
logger.exception(f"Exception occurred: {exc}")
7194

95+
async def _pubsub_data_reader_for_user(self, pubsub_subscriber):
96+
try:
97+
while True:
98+
message = await pubsub_subscriber.get_message(ignore_subscribe_messages=True)
99+
if message is not None:
100+
chat_guid = message["channel"].decode("utf-8")
101+
sockets = self.user_channel.get(chat_guid)
102+
if sockets:
103+
for socket in sockets:
104+
data = message["data"].decode("utf-8")
105+
await socket.send_text(data)
106+
except Exception as exc:
107+
logger.exception(f"Exception occurred: {exc}")
108+
72109
async def send_error(self, message: str, websocket: WebSocket):
73110
await websocket.send_json({"status": "error", "message": message})

Diff for: src/websocket/handlers.py

+24-20
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
import asyncio
21
import logging
32
from datetime import datetime
43

54
import redis.asyncio as aioredis
65
from fastapi import WebSocket
7-
from fastapi.encoders import jsonable_encoder
86
from sqlalchemy.ext.asyncio import AsyncSession
97

108
from src.managers.websocket_manager import WebSocketManager
@@ -23,7 +21,7 @@
2321
get_message_by_guid,
2422
mark_last_read_message,
2523
mark_user_as_online,
26-
send_new_chat_created_ws_message,
24+
notify_friend_about_new_chat,
2725
)
2826

2927
logger = logging.getLogger(__name__)
@@ -50,16 +48,18 @@ async def new_message_handler(
5048
message_schema = ReceiveMessageSchema(**incoming_message)
5149
chat_guid: str = str(message_schema.chat_guid)
5250

53-
notify_friend_about_new_chat: bool = False
51+
# notify_friend_about_new_chat: bool = False
5452
# newly created chat
5553
if not chats or chat_guid not in chats:
54+
print("YES", chats)
5655
chat_id: int | None = await get_chat_id_by_guid(db_session, chat_guid=chat_guid)
5756
if chat_id:
57+
print("Chat ID", chat_id)
5858
# this action modifies chats variable in websocket view
5959
chats[chat_guid] = chat_id
6060
await socket_manager.add_user_to_chat(chat_guid, websocket)
6161
# must notify friend that new chat has been created
62-
notify_friend_about_new_chat = True
62+
# notify_friend_about_new_chat = True
6363

6464
else:
6565
await socket_manager.send_error("Chat has not been added", websocket)
@@ -113,9 +113,11 @@ async def new_message_handler(
113113

114114
await socket_manager.broadcast_to_chat(chat_guid, outgoing_message)
115115

116-
if notify_friend_about_new_chat:
117-
logger.info("Notifying friend about newly created chat")
118-
await send_new_chat_created_ws_message(socket_manager=socket_manager, current_user=current_user, chat=chat)
116+
await notify_friend_about_new_chat(socket_manager=socket_manager, current_user=current_user, chat=chat)
117+
118+
# if notify_friend_about_new_chat:
119+
# logger.info("Notifying friend about newly created chat")
120+
# await send_new_chat_created_ws_message(socket_manager=socket_manager, current_user=current_user, chat=chat)
119121

120122

121123
@socket_manager.handler("message_read")
@@ -248,22 +250,24 @@ async def chat_deleted_handler(
248250
# get all websocket connections that belong to this chat (except for ws that sent this messsage)
249251
# and send notification that chat has been removed
250252

251-
target_websockets: set[WebSocket] = socket_manager.chats.get(chat_guid)
252-
253253
outgoing_message = {
254254
"type": "chat_deleted",
255255
"user_guid": str(current_user.guid),
256256
"user_name": current_user.first_name,
257257
"chat_guid": chat_guid,
258258
}
259259

260-
if target_websockets:
261-
# Send the notification message to the target user concurrently
262-
# used to notify frontend
263-
await asyncio.gather(
264-
*[
265-
socket.send_json(jsonable_encoder(outgoing_message))
266-
for socket in target_websockets
267-
if socket != websocket
268-
]
269-
)
260+
await socket_manager.broadcast_to_chat(chat_guid, outgoing_message)
261+
262+
# target_websockets: set[WebSocket] = socket_manager.chats.get(chat_guid)
263+
264+
# if target_websockets:
265+
# # Send the notification message to the target user concurrently
266+
# # used to notify frontend
267+
# await asyncio.gather(
268+
# *[
269+
# socket.send_json(jsonable_encoder(outgoing_message))
270+
# for socket in target_websockets
271+
# if socket != websocket
272+
# ]
273+
# )

Diff for: src/websocket/router.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ async def websocket_endpoint(
5252
else:
5353
chats = dict()
5454

55+
# add user socket connection to user channel
56+
await socket_manager.create_user_channel(user_guid=str(current_user.guid), websocket=websocket)
57+
logger.debug(f"User channels {socket_manager.user_channel}")
58+
5559
# task for sending status messages, not dependent on cache_enabled
5660
user_status_task = asyncio.create_task(check_user_statuses(cache, socket_manager, current_user, chats))
5761

@@ -96,8 +100,9 @@ async def websocket_endpoint(
96100
await socket_manager.send_error("You have sent too many requests", websocket)
97101

98102
except WebSocketDisconnect:
99-
logging.info("Websocket is disconnected")
103+
logger.info("Websocket is disconnected")
100104
# unsubscribe user websocket connection from all chats
105+
await socket_manager.remove_websocket_from_user_channel(user_guid=str(current_user.guid), websocket=websocket)
101106
if chats:
102107
for chat_guid in chats:
103108
await socket_manager.remove_user_from_chat(chat_guid, websocket)

Diff for: src/websocket/schemas.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ class UserTypingSchema(BaseModel):
3434

3535

3636
class NewChatCreated(BaseModel):
37-
type: str = "new_chat_created"
37+
type: str = "new_chat_created" # handled by frontend only
3838
chat_id: int # need to pass for guid/id mapping [chats variable]
3939
chat_guid: UUID4
4040
created_at: datetime

Diff for: src/websocket/services.py

+44
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,50 @@ async def get_chat_id_by_guid(db_session: AsyncSession, *, chat_guid: UUID) -> i
155155
return chat_id
156156

157157

158+
async def notify_friend_about_new_chat(socket_manager: WebSocketManager, current_user: User, chat: Chat):
159+
# get friend guid
160+
161+
friend_guid: UUID | None = next((user.guid for user in chat.users if not user == current_user), None)
162+
if not friend_guid:
163+
logger.error("Friend guid not found", extra={"type": "new_chat_created", "friend_guid": friend_guid})
164+
return
165+
logger.debug(f"BROADCASTING TO FRIEND {friend_guid}")
166+
await socket_manager.broadcast_to_user(str(friend_guid), {"Message": "WTF"})
167+
168+
# check if friend has active websocket connections
169+
logger.debug(f"User channels {socket_manager.user_channel}")
170+
# friend_websockets: set[WebSocket] = socket_manager.user_channel.get(str(friend_guid))
171+
172+
# if not friend_websockets:
173+
# logger.info(
174+
# "Friend doesn't have active connections", extra={"type": "new_chat_created", "friend_guid": friend_guid}
175+
# )
176+
# return
177+
178+
current_user_info: dict = {
179+
"guid": current_user.guid,
180+
"first_name": current_user.first_name,
181+
"last_name": current_user.last_name,
182+
"username": current_user.username,
183+
"user_image": current_user.user_image,
184+
}
185+
payload = NewChatCreated(
186+
chat_id=chat.id,
187+
chat_guid=chat.guid,
188+
created_at=chat.created_at,
189+
updated_at=chat.updated_at,
190+
friend=current_user_info, # current user becomes a friend for a user that receives this message
191+
has_new_messages=True,
192+
new_messages_count=1,
193+
)
194+
195+
json_payload: str = payload.model_dump_json()
196+
print("Socket's chats", socket_manager.chats)
197+
print("Broadcasting to user", friend_guid, json_payload)
198+
await socket_manager.broadcast_to_user(str(friend_guid), json_payload)
199+
await socket_manager.broadcast_to_user(str(current_user.guid), json_payload)
200+
201+
158202
async def send_new_chat_created_ws_message(socket_manager: WebSocketManager, current_user: User, chat: Chat):
159203
"""
160204
Send a new chat created message to friend's websocket connections in the chat.

0 commit comments

Comments
 (0)