diff --git a/chat/consumers.py b/chat/consumers.py new file mode 100644 index 0000000..7af3987 --- /dev/null +++ b/chat/consumers.py @@ -0,0 +1,46 @@ +# Copyright (C) 2024 by Animath +# SPDX-License-Identifier: GPL-3.0-or-later + +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from registration.models import Registration + + +class ChatConsumer(AsyncJsonWebsocketConsumer): + """ + This consumer manages the websocket of the chat interface. + """ + async def connect(self) -> None: + """ + This function is called when a new websocket is trying to connect to the server. + We accept only if this is a user of a team of the associated tournament, or a volunteer + of the tournament. + """ + + # Fetch the registration of the current user + user = self.scope['user'] + if user.is_anonymous: + # User is not authenticated + await self.close() + return + + reg = await Registration.objects.aget(user_id=user.id) + self.registration = reg + + # Accept the connection + await self.accept() + + async def disconnect(self, close_code) -> None: + """ + Called when the websocket got disconnected, for any reason. + :param close_code: The error code. + """ + if self.scope['user'].is_anonymous: + # User is not authenticated + return + + async def receive_json(self, content, **kwargs): + """ + Called when the client sends us some data, parsed as JSON. + :param content: The sent data, decoded from JSON text. Must content a `type` field. + """ + # TODO Process chat protocol diff --git a/chat/static/chat.js b/chat/static/chat.js new file mode 100644 index 0000000..5a45a83 --- /dev/null +++ b/chat/static/chat.js @@ -0,0 +1,52 @@ +(async () => { + // check notification permission + // This is useful to alert people that they should do something + await Notification.requestPermission() +})() + +/** + * Display a new notification with the given title and the given body. + * @param title The title of the notification + * @param body The body of the notification + * @param timeout The time (in milliseconds) after that the notification automatically closes. 0 to make indefinite. Default to 5000 ms. + * @return Notification + */ +function showNotification(title, body, timeout = 5000) { + let notif = new Notification(title, {'body': body, 'icon': "/static/tfjm.svg"}) + if (timeout) + setTimeout(() => notif.close(), timeout) + return notif +} + +document.addEventListener('DOMContentLoaded', () => { + /** + * Process the received data from the server. + * @param data The received message + */ + function processMessage(data) { + // TODO Implement chat protocol + } + + function setupSocket(nextDelay = 1000) { + // Open a global websocket + socket = new WebSocket( + (document.location.protocol === 'https:' ? 'wss' : 'ws') + '://' + window.location.host + '/ws/chat/' + ) + + // Listen on websockets and process messages from the server + socket.addEventListener('message', e => { + // Parse received data as JSON + const data = JSON.parse(e.data) + + processMessage(data) + }) + + // Manage errors + socket.addEventListener('close', e => { + console.error('Chat socket closed unexpectedly, restarting…') + setTimeout(() => setupSocket(2 * nextDelay), nextDelay) + }) + } + + setupSocket() +}) diff --git a/chat/templates/chat/chat.html b/chat/templates/chat/chat.html new file mode 100644 index 0000000..cf7de49 --- /dev/null +++ b/chat/templates/chat/chat.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} + +{% load static %} +{% load i18n %} + +{% block content %} +{% endblock %} + +{% block extrajavascript %} + {# This script contains all data for the chat management #} + +{% endblock %} diff --git a/chat/urls.py b/chat/urls.py index 80ea069..77d52c2 100644 --- a/chat/urls.py +++ b/chat/urls.py @@ -1,2 +1,13 @@ # Copyright (C) 2024 by Animath # SPDX-License-Identifier: GPL-3.0-or-later + +from django.urls import path + +from .views import ChatView + + +app_name = 'chat' + +urlpatterns = [ + path('', ChatView.as_view(), name='chat'), +] diff --git a/chat/views.py b/chat/views.py index 80ea069..8c4ef96 100644 --- a/chat/views.py +++ b/chat/views.py @@ -1,2 +1,13 @@ # Copyright (C) 2024 by Animath # SPDX-License-Identifier: GPL-3.0-or-later + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import TemplateView + + +class ChatView(LoginRequiredMixin, TemplateView): + """ + This view is the main interface of the chat system, which is working + with Javascript and websockets. + """ + template_name = "chat/chat.html" diff --git a/draw/routing.py b/draw/routing.py deleted file mode 100644 index 8ce6085..0000000 --- a/draw/routing.py +++ /dev/null @@ -1,10 +0,0 @@ -# Copyright (C) 2023 by Animath -# SPDX-License-Identifier: GPL-3.0-or-later - -from django.urls import path - -from . import consumers - -websocket_urlpatterns = [ - path("ws/draw/", consumers.DrawConsumer.as_asgi()), -] diff --git a/draw/tests.py b/draw/tests.py index 9f3ec6a..bbb209a 100644 --- a/draw/tests.py +++ b/draw/tests.py @@ -14,8 +14,8 @@ from django.contrib.sites.models import Site from django.test import TestCase from django.urls import reverse from participation.models import Team, Tournament +from tfjm import routing as websocket_routing -from . import routing from .models import Draw, Pool, Round, TeamDraw @@ -55,7 +55,7 @@ class TestDraw(TestCase): # Connect to Websocket headers = [(b'cookie', self.async_client.cookies.output(header='', sep='; ').encode())] - communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns)), + communicator = WebsocketCommunicator(AuthMiddlewareStack(URLRouter(websocket_routing.websocket_urlpatterns)), "/ws/draw/", headers) connected, subprotocol = await communicator.connect() self.assertTrue(connected) diff --git a/participation/templates/participation/chat.html b/participation/templates/participation/chat.html deleted file mode 100644 index bf47704..0000000 --- a/participation/templates/participation/chat.html +++ /dev/null @@ -1,13 +0,0 @@ -{% extends "base.html" %} - -{% load i18n %} - -{% block content %} -