Skip to content
This repository was archived by the owner on Mar 23, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions FD/asgi.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'FD.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "FD.settings")
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

application = get_asgi_application()
from chat.routing import websocket_urlpatterns

application = ProtocolTypeRouter(
{
"http": django_asgi_app,
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
),
}
)
24 changes: 23 additions & 1 deletion FD/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
ALLOWED_HOSTS = ['*']

INSTALLED_APPS = [
'daphne',

'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
Expand All @@ -28,6 +30,7 @@
'corsheaders',
'storages',
'django_filters',
'channels',

'authentication',
'groups',
Expand Down Expand Up @@ -69,7 +72,9 @@
},
]

WSGI_APPLICATION = 'FD.wsgi.application'
# WSGI_APPLICATION = 'FD.wsgi.application'
ASGI_APPLICATION = "FD.asgi.application"


DATABASES = {
'default': {
Expand Down Expand Up @@ -158,3 +163,20 @@
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
},
}



import urllib.parse
import redis

redis_url = os.environ.get('REDIS_URL')
parsed_url = urllib.parse.urlparse(redis_url)

CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [f"rediss://{parsed_url.netloc}"],
},
},
}
99 changes: 99 additions & 0 deletions chat/consumers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync

from django.core.serializers.json import DjangoJSONEncoder

from rest_framework_simplejwt.tokens import AccessToken
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError

from .models import Chat, Message
from authentication.models import User

class ChatConsumer(WebsocketConsumer):
def connect(self):
self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
self.room_group_name = f"chat_{self.room_name}"

try:
self.chat = Chat.objects.get(group__title=self.room_name)
except Chat.DoesNotExist:
self.close()

headers = dict(self.scope['headers'])
auth_header = headers.get(b'authorization', b'').decode()

if auth_header.startswith('Bearer '):
token = auth_header.split(' ')[1]
user = self.get_user_from_token(token)
if user:
self.scope['user'] = user
else:
self.close()
return
else:
self.close()
return

# Join room group
async_to_sync(self.channel_layer.group_add)(
self.room_group_name, self.channel_name
)

self.accept()

messages = self.get_chat_messages()
self.send(text_data=json.dumps({
'type': 'chat_history',
'messages': messages
}, cls=DjangoJSONEncoder))


def get_chat_messages(self):
return list(self.chat.messages.all().order_by('timestamp').values(
'content', 'sender__username', 'timestamp'
))

def get_user_from_token(self, token):
try:
access_token = AccessToken(token)
user_id = access_token['user_id']
return User.objects.get(id=user_id)
except (InvalidToken, TokenError, User.DoesNotExist):
return None

def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name, self.channel_name
)

def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json["message"]
user = self.scope["user"]

db_message = Message.objects.create(
chat=self.chat,
content=message,
sender=user
)

async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
"type": "chat_message",
"message": db_message.content,
"username": db_message.sender.username,
"timestamp": db_message.timestamp.isoformat(),
}
)

# Receive message from room group
def chat_message(self, event):
# Send message to WebSocket
self.send(text_data=json.dumps({
'message': event['message'],
'username': event['username'],
'timestamp': event['timestamp']
}))
7 changes: 7 additions & 0 deletions chat/routing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
]
51 changes: 51 additions & 0 deletions chat/templates/chat/room.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<!-- chat/templates/chat/room.html -->
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name_json|json_script:"room-name" }}
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);

const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/chat/'
+ encodeURIComponent(roomName.replace(/"/g, ''))
+ '/'
);

chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.username + ': ' + data.message + '\n');
};

chatSocket.onclose = function(e) {
console.error('Chat socket closed unexpectedly');
};

document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function(e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};

document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</body>
</html>
7 changes: 5 additions & 2 deletions chat/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from .views import *

urlpatterns = [
path('<str:title>/send/', ChatView.as_view()),
path('<str:title>/retrieve/', ChatView.as_view()),
# path('<str:title>/send/', ChatView.as_view()),
# path('<str:title>/retrieve/', ChatView.as_view()),

path('<str:room_name>/', chat_room, name='chat_room'),

]
56 changes: 34 additions & 22 deletions chat/views.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,38 @@
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
# from rest_framework.views import APIView
# from rest_framework.response import Response
# from rest_framework import status
# from rest_framework.permissions import IsAuthenticated

from .serializers import *
from .services import *
# from .serializers import *
# from .services import *

class ChatView(APIView):
permission_classes = [IsAuthenticated]
serializer_class = ChatSerializer
# class ChatView(APIView):
# permission_classes = [IsAuthenticated]
# serializer_class = ChatSerializer

def get(self, request, *args, **kwargs):
try:
messages = ChatService.get_messages(kwargs.get('title'))
serializer = self.serializer_class(messages, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
except ValidationError as e:
return Response(e.detail.get('detail'), status=e.detail.get('status'))
# def get(self, request, *args, **kwargs):
# try:
# messages = ChatService.get_messages(kwargs.get('title'))
# serializer = self.serializer_class(messages, many=True)
# return Response(serializer.data, status=status.HTTP_200_OK)
# except ValidationError as e:
# return Response(e.detail.get('detail'), status=e.detail.get('status'))

def post(self, request, *args, **kwargs):
try:
message = ChatService.send_message(request.user, kwargs.get('title'), request.data['content'])
return Response(self.serializer_class(message).data, status=status.HTTP_201_CREATED)
except ValidationError as e:
return Response(e.detail.get('detail'), status=e.detail.get('status'))
# def post(self, request, *args, **kwargs):
# try:
# message = ChatService.send_message(request.user, kwargs.get('title'), request.data['content'])
# return Response(self.serializer_class(message).data, status=status.HTTP_201_CREATED)
# except ValidationError as e:
# return Response(e.detail.get('detail'), status=e.detail.get('status'))


from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.utils.safestring import mark_safe
import json

@login_required
def chat_room(request, room_name):
return render(request, 'chat/room.html', {
'room_name_json': mark_safe(json.dumps(room_name))
})
23 changes: 23 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,30 +1,53 @@
asgiref==3.8.1
attrs==24.2.0
autobahn==24.4.2
Automat==24.8.1
boto3==1.35.64
botocore==1.35.64
cffi==1.17.1
channels==4.2.0
channels_redis==4.2.1
constantly==23.10.4
cryptography==43.0.3
daphne==4.1.2
Django==5.1.2
django-cors-headers==4.6.0
django-filter==24.3
django-storages==1.14.4
djangorestframework==3.15.2
djangorestframework-simplejwt==5.3.1
drf-spectacular==0.27.2
hyperlink==21.0.0
idna==3.10
incremental==24.7.2
inflection==0.5.1
jmespath==1.0.1
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
msgpack==1.1.0
mysql-connector-python==9.1.0
mysqlclient==2.2.5
pillow==11.0.0
pyasn1==0.6.1
pyasn1_modules==0.4.1
pycparser==2.22
PyJWT==2.9.0
pyOpenSSL==24.2.1
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
PyYAML==6.0.2
redis==5.2.0
referencing==0.35.1
rpds-py==0.20.1
s3transfer==0.10.3
service-identity==24.2.0
setuptools==75.6.0
six==1.16.0
sqlparse==0.5.1
Twisted==24.10.0
txaio==23.1.1
typing_extensions==4.12.2
tzdata==2024.2
uritemplate==4.1.1
urllib3==2.2.3
zope.interface==7.1.1