From 52a86081981a35cfff152669e6978a87334d1934 Mon Sep 17 00:00:00 2001 From: Alexander-D-Karpov Date: Tue, 21 Jun 2022 11:36:55 +0300 Subject: [PATCH] added channels basic room consumer, minor updates for base game logic --- chess_backend/asgi.py | 21 ++++++--- chess_backend/settings.py | 94 +++++++++++++-------------------------- chess_backend/urls.py | 18 +------- game/models.py | 6 +-- requirements.txt | 41 ++++++++++++++++- room/consumers.py | 63 ++++++++++++++++++++++++++ room/middleware.py | 32 +++++++++++++ room/models.py | 10 +++++ room/routing.py | 8 ++++ 9 files changed, 203 insertions(+), 90 deletions(-) create mode 100644 room/consumers.py create mode 100644 room/middleware.py create mode 100644 room/routing.py diff --git a/chess_backend/asgi.py b/chess_backend/asgi.py index ed24f6e..be4c508 100644 --- a/chess_backend/asgi.py +++ b/chess_backend/asgi.py @@ -9,12 +9,21 @@ https://docs.djangoproject.com/en/4.0/howto/deployment/asgi/ import os +from channels.auth import AuthMiddlewareStack +from channels.security.websocket import OriginValidator from django.core.asgi import get_asgi_application -from channels.routing import ProtocolTypeRouter +from channels.routing import ProtocolTypeRouter, URLRouter -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chess_backend.settings') -django_asgi_app = get_asgi_application() +import room.routing -application = ProtocolTypeRouter({ - "http": django_asgi_app, -}) +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "chess_backend.settings") + +application = ProtocolTypeRouter( + { + "http": get_asgi_application(), + "websocket": OriginValidator( + AuthMiddlewareStack(URLRouter(room.routing.websocket_urlpatterns)), + ["*"], + ), + } +) diff --git a/chess_backend/settings.py b/chess_backend/settings.py index f5b5ca9..d332280 100644 --- a/chess_backend/settings.py +++ b/chess_backend/settings.py @@ -1,39 +1,19 @@ -""" -Django settings for chess_backend project. - -Generated by 'django-admin startproject' using Django 4.0.3. - -For more information on this file, see -https://docs.djangoproject.com/en/4.0/topics/settings/ - -For the full list of settings and their values, see -https://docs.djangoproject.com/en/4.0/ref/settings/ -""" import os from pathlib import Path # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.0/howto/deployment/checklist/ - # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = "django-insecure-%_8sy196w4hzo9^cp9(@r=i+amh47r4mxfhq_(ok&=c(@%bhmk" TOKEN_EXP = 2678400 # 31 day - -# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] -# Application definition - INSTALLED_APPS = [ - "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", - "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", # Packages @@ -45,36 +25,26 @@ INSTALLED_APPS = [ MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", ] ROOT_URLCONF = "chess_backend.urls" -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [BASE_DIR / "templates"], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], +ASGI_APPLICATION = "chess_backend.asgi.application" +CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [("127.0.0.1", 6379)], }, }, -] -ASGI_APPLICATION = 'chess_backend.asgi.application' +} + WSGI_APPLICATION = "chess_backend.wsgi.application" -# Database -# https://docs.djangoproject.com/en/4.0/ref/settings/#databases DATABASES = { "default": { @@ -83,30 +53,12 @@ DATABASES = { } } -# Password validation -# https://docs.djangoproject.com/en/4.0/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, +LANGUAGES = [ + ("en-us", "English"), + ("ru", "Russian"), ] -# Internationalization -# https://docs.djangoproject.com/en/4.0/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" +TIME_ZONE = "Europe/Moscow" USE_I18N = True @@ -122,7 +74,23 @@ else: MEDIA_ROOT = "/var/www/media/" STATIC_ROOT = "/var/www/static/" -# Default primary key field type -# https://docs.djangoproject.com/en/4.0/ref/settings/#default-auto-field - DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "handlers": { + "file": { + "level": "WARNING", + "class": "logging.FileHandler", + "filename": "debug.log", + }, + }, + "loggers": { + "django": { + "handlers": ["file"], + "level": "DEBUG", + "propagate": True, + }, + }, +} diff --git a/chess_backend/urls.py b/chess_backend/urls.py index baefd3b..b810c4f 100644 --- a/chess_backend/urls.py +++ b/chess_backend/urls.py @@ -1,25 +1,9 @@ -"""chess_backend URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.0/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.conf import settings from django.conf.urls.static import static -from django.contrib import admin from django.urls import path, include urlpatterns = ( - [path("admin/", admin.site.urls), path("api/", include("game.urls"))] + [path("api/", include("game.urls"))] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) ) diff --git a/game/models.py b/game/models.py index 2633d17..954a3f2 100644 --- a/game/models.py +++ b/game/models.py @@ -103,13 +103,13 @@ class Hero(models.Model): "HeroImageSet", on_delete=models.CASCADE, related_name="die_image_fkey" ) health = models.IntegerField( - validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False + validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False ) attack = models.IntegerField( - validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False + validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False ) speed = models.IntegerField( - validators=[MinValueValidator(0), MaxValueValidator(10)], blank=False + validators=[MinValueValidator(1), MaxValueValidator(10)], blank=False ) def idle_img(self): diff --git a/requirements.txt b/requirements.txt index 080ec94..767bdf9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,46 @@ asgiref==3.5.2 +asttokens==2.0.5 +attrs==21.4.0 +autobahn==22.5.1 +Automat==20.2.0 +backcall==0.2.0 +cffi==1.15.0 +channels==3.0.4 +constantly==15.1.0 +cryptography==37.0.2 +daphne==3.0.2 +decorator==5.1.1 Django==4.0.5 djangorestframework==3.13.1 +executing==0.8.3 +hyperlink==21.0.0 +idna==3.3 +incremental==21.3.0 +ipython==8.4.0 +jedi==0.18.1 +matplotlib-inline==0.1.3 +parso==0.8.3 +pexpect==4.8.0 +pickleshare==0.7.5 Pillow==9.1.1 +prompt-toolkit==3.0.29 +ptyprocess==0.7.0 +pure-eval==0.2.2 +pyasn1==0.4.8 +pyasn1-modules==0.2.8 +pycparser==2.21 +Pygments==2.12.0 +PyJWT==2.4.0 +pyOpenSSL==22.0.0 pytz==2022.1 +service-identity==21.1.0 +six==1.16.0 sqlparse==0.4.2 -PyJWT==2.4.0 \ No newline at end of file +stack-data==0.2.0 +traitlets==5.2.2.post1 +Twisted==22.4.0 +txaio==22.2.1 +typing_extensions==4.2.0 +wcwidth==0.2.5 +zope.interface==5.4.0 +channels_redis diff --git a/room/consumers.py b/room/consumers.py new file mode 100644 index 0000000..87b627e --- /dev/null +++ b/room/consumers.py @@ -0,0 +1,63 @@ +import json +from channels.generic.websocket import AsyncWebsocketConsumer + + +class QueConsumer(AsyncWebsocketConsumer): + def __init__(self, *args, **kwargs): + super().__init__(args, kwargs) + self.room_group_name = None + + async def connect(self): + self.room_group_name = "que" + + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + + await self.accept() + + async def disconnect(self, close_code): + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + # Receive message from WebSocket + async def receive(self, text_data): + print(text_data) + # Send message to room group + await self.channel_layer.group_send( + self.room_group_name, + {"type": "chat_message", "message": text_data}, + ) + + +class RoomConsumer(AsyncWebsocketConsumer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.room_group_name = None + self.room_name = None + + async def connect(self): + self.room_name = self.scope["url_route"]["kwargs"]["room_name"] + self.room_group_name = "room_%s" % self.room_name + + # Join room group + await self.channel_layer.group_add(self.room_group_name, self.channel_name) + + await self.accept() + + async def disconnect(self, close_code): + # Leave room group + await self.channel_layer.group_discard(self.room_group_name, self.channel_name) + + # Receive message from WebSocket + async def receive(self, text_data): + print(text_data) + # Send message to room group + await self.channel_layer.group_send( + self.room_group_name, + {"type": "chat_message", "message": text_data}, + ) + + # Receive message from room group + async def chat_message(self, event): + message = event["message"] + + # Send message to WebSocket + await self.send(text_data=json.dumps({"lot": message})) diff --git a/room/middleware.py b/room/middleware.py new file mode 100644 index 0000000..e93e122 --- /dev/null +++ b/room/middleware.py @@ -0,0 +1,32 @@ +from channels.db import database_sync_to_async +from django.core.exceptions import PermissionDenied + +from game.models import Player +from game.services.jwt import read_jwt + + +@database_sync_to_async +def get_player(jwt): + payload = read_jwt(jwt) + if not payload: + raise PermissionDenied + try: + return Player.objects.get(id=payload) + except User.DoesNotExist: + return AnonymousUser() + + +class QueryAuthMiddleware: + """Custom middleware to read user auth token from string.""" + + def __init__(self, app): + # Store the ASGI application we were passed + self.app = app + + async def __call__(self, scope, receive, send): + # Look up user from query string (you should also do things like + # checking if it is a valid user ID, or if scope["user"] is already + # populated). + scope["user"] = await get_user(int(scope["query_string"])) + + return await self.app(scope, receive, send) diff --git a/room/models.py b/room/models.py index 71a8362..35940fc 100644 --- a/room/models.py +++ b/room/models.py @@ -1,3 +1,13 @@ from django.db import models # Create your models here. +from game.models import Player + + +class PlayerInQue(models.Model): + # TODO use redis for storing + player = models.ForeignKey(Player, unique=True, on_delete=models.CASCADE) + score = models.IntegerField() + + def __str__(self): + return f"{self.player.name} in que with score {self.score}" diff --git a/room/routing.py b/room/routing.py new file mode 100644 index 0000000..c74db1e --- /dev/null +++ b/room/routing.py @@ -0,0 +1,8 @@ +from django.urls import path + +from . import consumers + +websocket_urlpatterns = [ + path("room", consumers.QueConsumer.as_asgi()), + path("room//", consumers.RoomConsumer.as_asgi()), +]