Skip to content

Commit

Permalink
[Feat] create websocket for user status online/offline
Browse files Browse the repository at this point in the history
  • Loading branch information
zakarm committed May 2, 2024
1 parent 6c828b6 commit 4745017
Show file tree
Hide file tree
Showing 12 changed files with 2,020 additions and 73 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ GOOGLE_REDIRECT_URI=http://localhost:8000/api/social/google/callback

FORTYTWO_CLIENT_ID=u-s4t2ud-03320effd62010122a41a9afc7769040aa73abca5a65ebf19ac35ea352653b6a
FORTYTWO_CLIENT_SECRET=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
FORTYTWO_REDIRECT_URI=http://localhost:8000/api/social/42/callback
FORTYTWO_REDIRECT_URI=http://localhost:8000/api/social/42/callback
1 change: 1 addition & 0 deletions app/back-end/authentication/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class User(AbstractBaseUser, PermissionsMixin):
last_name = models.CharField(max_length=30, blank=True, null=True)
is_staff = models.BooleanField(default=False)
is_active = models.BooleanField(default=True)
is_online = models.IntegerField(default = 0)
date_joined = models.DateTimeField(auto_now_add=True)
is_superuser = models.BooleanField(default=False)
last_login = models.DateTimeField(auto_now=True)
Expand Down
124 changes: 96 additions & 28 deletions app/back-end/dashboards/consumers.py
Original file line number Diff line number Diff line change
@@ -1,48 +1,116 @@
import sys
from channels.generic.websocket import AsyncWebsocketConsumer
import json
from django.contrib.auth.models import AnonymousUser
from channels.generic.websocket import AsyncWebsocketConsumer
from rest_framework_simplejwt.tokens import AccessToken
from .serializer import FriendsSerializer
from django.contrib.auth import get_user_model
from django.contrib.auth.models import AnonymousUser
from channels.db import database_sync_to_async
from authentication.models import User
from django.db.models import F

@database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()

@database_sync_to_async
def get_friends(user_id):
return FriendsSerializer(instance=User.objects.get(id=user_id)).data

@database_sync_to_async
def update_user_online(user_id):
try:
User.objects.filter(id=user_id).update(is_online = F('is_online') + 1)
except User.DoesNotExist:
pass

@database_sync_to_async
def update_user_offline(user_id):
try:
User.objects.filter(id=user_id).update(is_online = F('is_online') - 1)
except User.DoesNotExist:
pass

async def send_online_notification(self, user_id):
await self.channel_layer.group_send(
"online_users",
{
"type": "send_online_notification",
"user_id": user_id,
}
)

async def send_online_notification_to_socket(self, event):
user_id = event["user_id"]
user = await get_user(user_id)
await self.send(text_data=json.dumps({
"type": "online_notification",
"user": {
"id": user.id,
"username": user.username,
"image_url": user.image_url,
# Add any other relevant user data
}
}))

async def send_online_notification(self, event):
await self.send_online_notification_to_socket(event)

class UserStatusConsumer(AsyncWebsocketConsumer):
async def connect(self):
# Get the user and authentication data from the scope
user = self.scope["user"]
auth_headers = self.scope.get("headers", [])
auth_header = None
for header in auth_headers:
if header[0].decode() == "authorization":
if header[0].decode() == "access":
auth_header = header[1].decode()
break

if user.is_authenticated:
# User is authenticated
print(f"Authenticated user: {user.username}", file=sys.stderr)
self.accept()
elif auth_header:
# Try to authenticate the user with the provided token
if auth_header:
try:
token = auth_header.split(" ")[1]
access_token = AccessToken(token)
user = access_token.payload["user_id"]
print(f"Authenticated user: {user}", file=sys.stderr)
self.accept()
access_token = AccessToken(auth_header)
user_id = access_token.payload["user_id"]
print(user_id, file = sys.stderr)
self.user = get_user(user_id)
self.user_id = user_id
await self.accept()
await update_user_online(self.user_id)
await self.send_online_notification(self.user_id) # Send online notification
await self.channel_layer.group_add(
"online_users",
self.channel_name
)
except Exception as e:
print(f"Authentication error: {e}", file=sys.stderr)
self.close()
await self.close()
else:
# Anonymous user
print("Anonymous user", file=sys.stderr)
self.accept()
await self.accept()

async def disconnect(self, close_code):
pass
await self.channel_layer.group_discard(
"online_users",
self.channel_name
)
await update_user_offline(self.user_id)

async def receive(self, text_data):
print(f"Received message: {text_data}", file=sys.stderr)
try:
text_data_json = json.loads(text_data)
message = text_data_json['message']
except json.JSONDecodeError:
message = text_data
await self.send(text_data=json.dumps({'message': message}))
if self.user:
try:
text_data_json = json.loads(text_data)
if text_data_json.get("action") == "get_friends":
data = await get_friends(self.user_id)
await self.send(text_data=json.dumps({
"friends": data
}))
else:
await self.send(text_data=json.dumps({
"error": "Invalid action"
}))
except json.JSONDecodeError:
await self.send(text_data=json.dumps({
"error": "Invalid JSON data"
}))
else:
await self.close()
30 changes: 0 additions & 30 deletions app/back-end/dashboards/jwt_middleware_auth.py

This file was deleted.

2 changes: 1 addition & 1 deletion app/back-end/dashboards/serializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'username')

class FriendshipSerializer(serializers.ModelSerializer):
user = serializers.SerializerMethodField()
class Meta:
Expand Down
4 changes: 1 addition & 3 deletions app/back-end/ft_transcendence/asgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import django
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
from dashboards.jwt_middleware_auth import JwtAthenticationMiddleware
from dashboards import routing as dashboard_routing
from game import routing as game_routing

Expand All @@ -20,12 +19,11 @@

application = ProtocolTypeRouter({
'http': get_asgi_application(),
'websocket': JwtAthenticationMiddleware(
'websocket':
URLRouter
(
dashboard_routing.websocket_urlpatterns
+
game_routing.websocket_urlpatterns
)
)
})
6 changes: 2 additions & 4 deletions app/back-end/ft_transcendence/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
'corsheaders',
'rest_framework_simplejwt.token_blacklist',
'channels',
'djangochannelsrestframework'
'djangochannelsrestframework',
'channels_redis'
]

MIDDLEWARE = [
Expand Down Expand Up @@ -104,9 +105,6 @@
'rest_framework.authentication.SessionAuthentication',
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
# 'DEFAULT_PERMISSION_CLASSES': [
# 'rest_framework.permissions.IsAuthenticated',
# ],
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 50,
}
Expand Down
3 changes: 1 addition & 2 deletions app/back-end/game/routing.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from django.urls import re_path
from . import consumers
from dashboards.jwt_middleware_auth import JwtAthenticationMiddleware

websocket_urlpatterns = [
re_path(r'ws/data/$', JwtAthenticationMiddleware(consumers.GameConsumer.as_asgi())),
re_path(r'ws/data$', consumers.GameConsumer.as_asgi()),
]
4 changes: 3 additions & 1 deletion app/back-end/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ asgiref
daphne
requests-mock
channels
faker
faker
asgi_redis
channels_redis
22 changes: 19 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ version: '3.8'
services:
back-end:
container_name: back-end
image: back-end
image: back-end:42
build:
context: ./app/back-end
dockerfile: Dockerfile
Expand All @@ -22,7 +22,7 @@ services:

front-end:
container_name: front-end
image: front-end
image: front-end:42
build:
context: ./app/front-end
dockerfile: Dockerfile
Expand All @@ -43,7 +43,7 @@ services:

data-base:
container_name: data-base
image: data-base
image: data-base:42
build:
context: ./app/data-base
dockerfile: Dockerfile
Expand All @@ -56,6 +56,22 @@ services:
- v_data-base:/var/lib/postgresql/data
networks:
- ft_transcendence

redis:
container_name: redis
image: redis:42
build:
context: ./services/redis
dockerfile: Dockerfile
restart: on-failure
depends_on:
- back-end
ports:
- "6379:6379"
env_file:
- .env
networks:
- ft_transcendence

volumes:
v_data-base:
Expand Down
12 changes: 12 additions & 0 deletions services/redis/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM debian:bullseye

RUN apt-get update -y && apt-get install -y \
--no-install-recommends \
redis-server=5:6.0.16-1+deb11u2 \
&& apt-get clean && rm -rf /var/lib/apt/lists/*

COPY ./conf/redis.conf /etc/redis/

EXPOSE 6379

CMD ["redis-server", "--protected-mode", "no"]
Loading

0 comments on commit 4745017

Please sign in to comment.