Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main'
Browse files Browse the repository at this point in the history
  • Loading branch information
evnsh committed Oct 31, 2024
2 parents 598e3dd + 470b58d commit bd170a2
Show file tree
Hide file tree
Showing 4 changed files with 366 additions and 9 deletions.
6 changes: 5 additions & 1 deletion backend/api/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,17 @@ def create_ai_account(sender, **kwargs):
User.objects.create(
userID="user_ai",
username='ai',
displayName='Prune',
displayName='Prune 🤖',
email='prune@brandoncodes.dev',
password='',
lang='EN',
avatarID=f"data:image/jpeg;base64,{encoded_avatar}",
flags=3
)
else:
ai_account = User.objects.get(userID="user_ai")
ai_account.displayName = 'Prune 🤖'
ai_account.save()

settings, _ = UserSettings.objects.get_or_create(userID="user_ai")
store_items = StoreItem.objects.all()
Expand Down
348 changes: 348 additions & 0 deletions backend/api/consumers/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from django.db.models import Q
from django.utils import timezone
from django.conf import settings
from django.core.cache import cache

from ..models import Conversation, User, Relationship, Match, UserSettings, Tournament
from ..util import generate_id, get_safe_profile, get_user_id_from_token
Expand All @@ -23,6 +24,353 @@
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)

class StatusConsumer(AsyncWebsocketConsumer):

async def connect(self):
self.user = None
self.heartbeat_task = None
self.failed_heartbeats = 0
self.first_heartbeat = True

query_string = self.scope['query_string'].decode()
query_params = urllib.parse.parse_qs(query_string)
token = query_params.get('token', [None])[0]

if token is None:
logger.info(f"[{self.__class__.__name__}] Connection attempt without token")
await self.close()
return

userID = await get_user_id_from_token(token)

if userID is None:
logger.info(f"[{self.__class__.__name__}] Connection attempt with invalid token")
await self.close()
return

self.user = await sync_to_async(User.objects.get)(userID=userID)
self.user_group_name = f"status_{self.user.userID}"

connection_count_key = f"status_user_connections_{self.user.userID}"
connection_count = cache.get(connection_count_key, 0)
cache.set(connection_count_key, connection_count + 1, timeout=None)

await self.channel_layer.group_add(
self.user_group_name,
self.channel_name
)

await self.accept()
self.heartbeat_task = asyncio.create_task(self.check_heartbeat())
logger.info(f"[{self.__class__.__name__}] User {self.user.username} connected")

async def disconnect(self, close_code):

if self.heartbeat_task and not self.heartbeat_task.done():
self.heartbeat_task.cancel()
if self.user is not None:
connection_count_key = f"status_user_connections_{self.user.userID}"
connection_count = cache.get(connection_count_key, 0) - 1

await self.channel_layer.group_discard(
self.user_group_name,
self.channel_name
)

if connection_count > 0:
cache.set(connection_count_key, connection_count, timeout=None)
logger.info(f"[{self.__class__.__name__}] User {self.user.username} disconnected, {connection_count} connections remaining")
else:
cache.delete(connection_count_key)
await self.update_user_status(False, None)
await self.notify_friends_connection(self.user)
logger.info(f"[{self.__class__.__name__}] User {self.user.username} disconnected")

async def receive(self, text_data):
try:
json_data = json.loads(text_data)
message_type = json_data.get("type", None)

if not message_type:
raise Exception("Missing message type")

match message_type:
case "heartbeat":
activity = json_data.get("activity", None)

if activity is None:
raise Exception("Missing activity")
if activity not in ["HOME", "QUEUEING", "PLAYING_VS_AI", "PLAYING_MULTIPLAYER", "PLAYING_LOCAL"]:
raise Exception("Invalid activity, not in [HOME, QUEUEING, PLAYING_VS_AI, PLAYING_MULTIPLAYER, PLAYING_LOCAL]")

self.failed_heartbeats = 0
await self.update_user_status(True, activity)

if self.first_heartbeat:
await self.notify_friends_connection(self.user)
self.first_heartbeat = False

case _:
raise Exception(f"Invalid message type: {message_type}")

except Exception as err:
try:
await self.send(json.dumps({
"type": "error",
"message": "Invalid JSON",
"more_info": str(err)
}))
except Exception as _:
pass

@sync_to_async
def update_user_status(self, online, activity):
try:
self.user = User.objects.get(userID=self.user.userID)
except User.DoesNotExist:
return

self.user.status = {
"online": online,
"activity": activity,
"last_seen": timezone.now().isoformat()
}
self.user.save()

async def notify_friends_connection(self, user):
friends = await sync_to_async(list)(
Relationship.objects.filter(
Q(userA=user.userID) | Q(userB=user.userID),
status=1
)
)

for relationship in friends:
friend_id = relationship.userA if relationship.userA != user.userID else relationship.userB

try:
friend = await sync_to_async(User.objects.get)(userID=friend_id)
except User.DoesNotExist:
continue

await self.channel_layer.group_send(
f"status_{friend.userID}",
{
"type": "connection_event",
"user": get_safe_profile(UserSerializer(user).data, me=False)
}
)

async def connection_event(self, event):
try:
await self.send(json.dumps({
"type": "connection_event",
"user": event["user"]
}))
except Exception as _:
pass

async def check_heartbeat(self):
while True:
self.failed_heartbeats += 1

if self.failed_heartbeats >= 3:
logger.info(f"[{self.__class__.__name__}] User {self.user.username} missed 3 heartbeats, closing connection")
await self.close(code=4000)
break

try:
await self.send(json.dumps({
"type": "heartbeat"
}))
except Exception as _:
pass
await asyncio.sleep(2)

class ChatConsumer(AsyncWebsocketConsumer):

async def connect(self):
self.user = None

query_string = self.scope['query_string'].decode()
query_params = urllib.parse.parse_qs(query_string)
token = query_params.get('token', [None])[0]

if token is None:
logger.info(f"[{self.__class__.__name__}] Connection attempt without token")
await self.close()
return

userID = await get_user_id_from_token(token)

if userID is None:
logger.info(f"[{self.__class__.__name__}] Connection attempt with invalid token")
await self.close()
return

self.user = await sync_to_async(User.objects.get)(userID=userID)
self.user_group_name = f"chat_{self.user.userID}"

connection_count_key = f"chat_user_connections_{self.user.userID}"
connection_count = cache.get(connection_count_key, 0)
cache.set(connection_count_key, connection_count + 1, timeout=None)

await self.channel_layer.group_add(
self.user_group_name,
self.channel_name
)

if connection_count <= 0:
await self.ensure_conversations_exist(self.user)
logger.info(f"[{self.__class__.__name__}] User {self.user.username} connected, conversations ensured")
else:
logger.info(f"[{self.__class__.__name__}] User {self.user.username} reconnected")
await self.accept()

async def disconnect(self, close_code):
if self.user:
connection_count_key = f"chat_user_connections_{self.user.userID}"
connection_count = cache.get(connection_count_key, 0) - 1

await self.channel_layer.group_discard(
self.user_group_name,
self.channel_name
)

if connection_count > 0:
cache.set(connection_count_key, connection_count, timeout=None)
logger.info(f"[{self.__class__.__name__}] User {self.user.username} disconnected, {connection_count} connections remaining")
else:
cache.delete(connection_count_key)
logger.info(f"[{self.__class__.__name__}] User {self.user.username} disconnected")

async def receive(self, text_data):
try:
json_data = json.loads(text_data)
message_type = json_data.get("type", None)

if not message_type:
raise Exception("Missing message type")

match message_type:
case "send_message":
conversation_id = json_data.get("conversationID", None)
content = json_data.get("content", None)

if self.user is None:
raise Exception("Something went wrong when receiving the message")
if conversation_id is None:
raise Exception("Missing conversation ID")
if content is None:
raise Exception("Missing content")

logger.info(f"[{self.__class__.__name__}] Received message from {self.user.username}: {json_data}")
message = await self.add_message_to_conversation(conversation_id, self.user, content)
await self.notify_new_message(conversation_id, self.user, message)

case _:
raise Exception(f"Invalid message type: {message_type}")
except Exception as err:
try:
await self.send(json.dumps({
"type": "error",
"message": "Invalid JSON",
"more_info": str(err)
}))
except Exception as _:
pass

async def notify_new_message(self, conversation_id, sender, message):
conversation = await sync_to_async(Conversation.objects.get)(conversationID=conversation_id)

if not conversation:
raise Exception(f"Conversation {conversation_id} not found")

participants = await sync_to_async(list)(conversation.participants.all())
safe_profile = get_safe_profile(UserSerializer(sender).data, me=False)

for participant in participants:
participant_group_name = f"chat_{participant.userID}"

await self.channel_layer.group_send(
participant_group_name,
{
"type": "conversation_update",
"conversationID": conversation_id,
"sender": safe_profile,
"message": MessageSerializer(message).data
}
)

async def conversation_update(self, event):
try:
await self.send(json.dumps({
"type": "conversation_update",
"conversationID": event["conversationID"],
"sender": event["sender"],
"message": event["message"]
}))
except Exception as _:
pass

async def friend_request(self, event):
try:
if event["status"] == "accepted":
await self.ensure_conversations_exist(self.user)

await self.send(json.dumps({
"type": "friend_request",
"status": event["status"],
"data": event["data"]
}))
except Exception as _:
pass

@sync_to_async
def add_message_to_conversation(self, conversation_id, user, content):
conversation = Conversation.objects.get(conversationID=conversation_id)

if not conversation:
raise Exception(f"Conversation {conversation_id} not found")

if user not in conversation.participants.all():
raise Exception(f"User {user.username} is not part of conversation {conversation_id}")

message = conversation.messages.create(messageID=generate_id("msg"), sender=user, content=content)
conversation.save()
return message

@sync_to_async
def ensure_conversations_exist(self, user):
friends = Relationship.objects.filter(
Q(userA=user.userID) | Q(userB=user.userID)
)
friends = friends.exclude(
Q(status=2) | Q(status=0),
Q(userA=user.userID) | Q(userB=user.userID)
)

user = User.objects.get(userID=user.userID)

for relationship in friends:
friend_id = relationship.userA if relationship.userA != user.userID else relationship.userB

try:
friend = User.objects.get(userID=friend_id)
except User.DoesNotExist:
continue

existing_conversation = Conversation.objects.filter(
participants__userID__in=[user.userID, friend_id],
conversationType='private_message'
).annotate(participant_count=Count('participants')).filter(participant_count=2).exists()

if not existing_conversation:
new_conversation = Conversation.objects.create(conversationID=generate_id("conv"), conversationType='private_message')
new_conversation.receipientID = user.userID
new_conversation.participants.add(user, friend)
new_conversation.save()

class MatchConsumer(AsyncJsonWebsocketConsumer):
active_matches = {}

Expand Down
4 changes: 3 additions & 1 deletion frontend/src/components/Home/styles/Contributors.styled.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ export const CardsContainer = styled.div`
align-items: center;
width: 90%;
max-width: 1200px;
flex-wrap: wrap;
`;

export const Cards = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 2rem;
width: 100%;
`;

export const Card = styled.div`
Expand Down
Loading

0 comments on commit bd170a2

Please sign in to comment.