diff options
| -rw-r--r-- | .gitignore | 264 | ||||
| -rw-r--r-- | .python-version | 1 | ||||
| -rw-r--r-- | README.md | 0 | ||||
| -rw-r--r-- | chat/__init__.py | 0 | ||||
| -rw-r--r-- | chat/asgi.py | 28 | ||||
| -rw-r--r-- | chat/consumers.py | 65 | ||||
| -rw-r--r-- | chat/routing.py | 7 | ||||
| -rw-r--r-- | chat/settings.py | 135 | ||||
| -rw-r--r-- | chat/templates/chat.html | 136 | ||||
| -rw-r--r-- | chat/templates/room.html | 451 | ||||
| -rw-r--r-- | chat/urls.py | 26 | ||||
| -rw-r--r-- | chat/views.py | 14 | ||||
| -rw-r--r-- | chat/wsgi.py | 16 | ||||
| -rwxr-xr-x | manage.py | 22 | ||||
| -rw-r--r-- | pyproject.toml | 11 | ||||
| -rw-r--r-- | requirements.txt | 3 | ||||
| -rw-r--r-- | uv.lock | 393 |
17 files changed, 1572 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ee6aff --- /dev/null +++ b/.gitignore | |||
| @@ -0,0 +1,264 @@ | |||
| 1 | # Created by https://www.toptal.com/developers/gitignore/api/pycharm+all,python | ||
| 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=pycharm+all,python | ||
| 3 | |||
| 4 | ### PyCharm+all ### | ||
| 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider | ||
| 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 | ||
| 7 | |||
| 8 | # User-specific stuff | ||
| 9 | .idea/**/workspace.xml | ||
| 10 | .idea/**/tasks.xml | ||
| 11 | .idea/**/usage.statistics.xml | ||
| 12 | .idea/**/dictionaries | ||
| 13 | .idea/**/shelf | ||
| 14 | |||
| 15 | # AWS User-specific | ||
| 16 | .idea/**/aws.xml | ||
| 17 | |||
| 18 | # Generated files | ||
| 19 | .idea/**/contentModel.xml | ||
| 20 | |||
| 21 | # Sensitive or high-churn files | ||
| 22 | .idea/**/dataSources/ | ||
| 23 | .idea/**/dataSources.ids | ||
| 24 | .idea/**/dataSources.local.xml | ||
| 25 | .idea/**/sqlDataSources.xml | ||
| 26 | .idea/**/dynamic.xml | ||
| 27 | .idea/**/uiDesigner.xml | ||
| 28 | .idea/**/dbnavigator.xml | ||
| 29 | |||
| 30 | # Gradle | ||
| 31 | .idea/**/gradle.xml | ||
| 32 | .idea/**/libraries | ||
| 33 | |||
| 34 | # Gradle and Maven with auto-import | ||
| 35 | # When using Gradle or Maven with auto-import, you should exclude module files, | ||
| 36 | # since they will be recreated, and may cause churn. Uncomment if using | ||
| 37 | # auto-import. | ||
| 38 | # .idea/artifacts | ||
| 39 | # .idea/compiler.xml | ||
| 40 | # .idea/jarRepositories.xml | ||
| 41 | # .idea/modules.xml | ||
| 42 | # .idea/*.iml | ||
| 43 | # .idea/modules | ||
| 44 | # *.iml | ||
| 45 | # *.ipr | ||
| 46 | |||
| 47 | # CMake | ||
| 48 | cmake-build-*/ | ||
| 49 | |||
| 50 | # Mongo Explorer plugin | ||
| 51 | .idea/**/mongoSettings.xml | ||
| 52 | |||
| 53 | # File-based project format | ||
| 54 | *.iws | ||
| 55 | |||
| 56 | # IntelliJ | ||
| 57 | out/ | ||
| 58 | |||
| 59 | # mpeltonen/sbt-idea plugin | ||
| 60 | .idea_modules/ | ||
| 61 | |||
| 62 | # JIRA plugin | ||
| 63 | atlassian-ide-plugin.xml | ||
| 64 | |||
| 65 | # Cursive Clojure plugin | ||
| 66 | .idea/replstate.xml | ||
| 67 | |||
| 68 | # SonarLint plugin | ||
| 69 | .idea/sonarlint/ | ||
| 70 | |||
| 71 | # Crashlytics plugin (for Android Studio and IntelliJ) | ||
| 72 | com_crashlytics_export_strings.xml | ||
| 73 | crashlytics.properties | ||
| 74 | crashlytics-build.properties | ||
| 75 | fabric.properties | ||
| 76 | |||
| 77 | # Editor-based Rest Client | ||
| 78 | .idea/httpRequests | ||
| 79 | |||
| 80 | # Android studio 3.1+ serialized cache file | ||
| 81 | .idea/caches/build_file_checksums.ser | ||
| 82 | |||
| 83 | ### PyCharm+all Patch ### | ||
| 84 | # Ignore everything but code style settings and run configurations | ||
| 85 | # that are supposed to be shared within teams. | ||
| 86 | |||
| 87 | .idea/* | ||
| 88 | |||
| 89 | !.idea/codeStyles | ||
| 90 | !.idea/runConfigurations | ||
| 91 | |||
| 92 | ### Python ### | ||
| 93 | # Byte-compiled / optimized / DLL files | ||
| 94 | __pycache__/ | ||
| 95 | *.py[cod] | ||
| 96 | *$py.class | ||
| 97 | |||
| 98 | # C extensions | ||
| 99 | *.so | ||
| 100 | |||
| 101 | # Distribution / packaging | ||
| 102 | .Python | ||
| 103 | build/ | ||
| 104 | develop-eggs/ | ||
| 105 | dist/ | ||
| 106 | downloads/ | ||
| 107 | eggs/ | ||
| 108 | .eggs/ | ||
| 109 | lib/ | ||
| 110 | lib64/ | ||
| 111 | parts/ | ||
| 112 | sdist/ | ||
| 113 | var/ | ||
| 114 | wheels/ | ||
| 115 | share/python-wheels/ | ||
| 116 | *.egg-info/ | ||
| 117 | .installed.cfg | ||
| 118 | *.egg | ||
| 119 | MANIFEST | ||
| 120 | |||
| 121 | # PyInstaller | ||
| 122 | # Usually these files are written by a python script from a template | ||
| 123 | # before PyInstaller builds the exe, so as to inject date/other infos into it. | ||
| 124 | *.manifest | ||
| 125 | *.spec | ||
| 126 | |||
| 127 | # Installer logs | ||
| 128 | pip-log.txt | ||
| 129 | pip-delete-this-directory.txt | ||
| 130 | |||
| 131 | # Unit test / coverage reports | ||
| 132 | htmlcov/ | ||
| 133 | .tox/ | ||
| 134 | .nox/ | ||
| 135 | .coverage | ||
| 136 | .coverage.* | ||
| 137 | .cache | ||
| 138 | nosetests.xml | ||
| 139 | coverage.xml | ||
| 140 | *.cover | ||
| 141 | *.py,cover | ||
| 142 | .hypothesis/ | ||
| 143 | .pytest_cache/ | ||
| 144 | cover/ | ||
| 145 | |||
| 146 | # Translations | ||
| 147 | *.mo | ||
| 148 | *.pot | ||
| 149 | |||
| 150 | # Django stuff: | ||
| 151 | *.log | ||
| 152 | local_settings.py | ||
| 153 | db.sqlite3 | ||
| 154 | db.sqlite3-journal | ||
| 155 | |||
| 156 | # Flask stuff: | ||
| 157 | instance/ | ||
| 158 | .webassets-cache | ||
| 159 | |||
| 160 | # Scrapy stuff: | ||
| 161 | .scrapy | ||
| 162 | |||
| 163 | # Sphinx documentation | ||
| 164 | docs/_build/ | ||
| 165 | |||
| 166 | # PyBuilder | ||
| 167 | .pybuilder/ | ||
| 168 | target/ | ||
| 169 | |||
| 170 | # Jupyter Notebook | ||
| 171 | .ipynb_checkpoints | ||
| 172 | |||
| 173 | # IPython | ||
| 174 | profile_default/ | ||
| 175 | ipython_config.py | ||
| 176 | |||
| 177 | # pyenv | ||
| 178 | # For a library or package, you might want to ignore these files since the code is | ||
| 179 | # intended to run in multiple environments; otherwise, check them in: | ||
| 180 | # .python-version | ||
| 181 | |||
| 182 | # pipenv | ||
| 183 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | ||
| 184 | # However, in case of collaboration, if having platform-specific dependencies or dependencies | ||
| 185 | # having no cross-platform support, pipenv may install dependencies that don't work, or not | ||
| 186 | # install all needed dependencies. | ||
| 187 | #Pipfile.lock | ||
| 188 | |||
| 189 | # poetry | ||
| 190 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. | ||
| 191 | # This is especially recommended for binary packages to ensure reproducibility, and is more | ||
| 192 | # commonly ignored for libraries. | ||
| 193 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control | ||
| 194 | #poetry.lock | ||
| 195 | |||
| 196 | # pdm | ||
| 197 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. | ||
| 198 | #pdm.lock | ||
| 199 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it | ||
| 200 | # in version control. | ||
| 201 | # https://pdm.fming.dev/#use-with-ide | ||
| 202 | .pdm.toml | ||
| 203 | |||
| 204 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm | ||
| 205 | __pypackages__/ | ||
| 206 | |||
| 207 | # Celery stuff | ||
| 208 | celerybeat-schedule | ||
| 209 | celerybeat.pid | ||
| 210 | |||
| 211 | # SageMath parsed files | ||
| 212 | *.sage.py | ||
| 213 | |||
| 214 | # Environments | ||
| 215 | .env | ||
| 216 | .venv | ||
| 217 | env/ | ||
| 218 | venv/ | ||
| 219 | ENV/ | ||
| 220 | env.bak/ | ||
| 221 | venv.bak/ | ||
| 222 | |||
| 223 | # Spyder project settings | ||
| 224 | .spyderproject | ||
| 225 | .spyproject | ||
| 226 | |||
| 227 | # Rope project settings | ||
| 228 | .ropeproject | ||
| 229 | |||
| 230 | # mkdocs documentation | ||
| 231 | /site | ||
| 232 | |||
| 233 | # mypy | ||
| 234 | .mypy_cache/ | ||
| 235 | .dmypy.json | ||
| 236 | dmypy.json | ||
| 237 | |||
| 238 | # Pyre type checker | ||
| 239 | .pyre/ | ||
| 240 | |||
| 241 | # pytype static type analyzer | ||
| 242 | .pytype/ | ||
| 243 | |||
| 244 | # Cython debug symbols | ||
| 245 | cython_debug/ | ||
| 246 | |||
| 247 | # PyCharm | ||
| 248 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can | ||
| 249 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore | ||
| 250 | # and can be added to the global gitignore or merged into this file. For a more nuclear | ||
| 251 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. | ||
| 252 | #.idea/ | ||
| 253 | |||
| 254 | ### Python Patch ### | ||
| 255 | # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration | ||
| 256 | poetry.toml | ||
| 257 | |||
| 258 | # ruff | ||
| 259 | .ruff_cache/ | ||
| 260 | |||
| 261 | # LSP config files | ||
| 262 | pyrightconfig.json | ||
| 263 | |||
| 264 | # End of https://www.toptal.com/developers/gitignore/api/pycharm+all,python | ||
diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version | |||
| @@ -0,0 +1 @@ | |||
| 3.13 | |||
diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/README.md | |||
diff --git a/chat/__init__.py b/chat/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/chat/__init__.py | |||
diff --git a/chat/asgi.py b/chat/asgi.py new file mode 100644 index 0000000..247393e --- /dev/null +++ b/chat/asgi.py | |||
| @@ -0,0 +1,28 @@ | |||
| 1 | """ | ||
| 2 | ASGI config for chat project. | ||
| 3 | |||
| 4 | It exposes the ASGI callable as a module-level variable named ``application``. | ||
| 5 | |||
| 6 | For more information on this file, see | ||
| 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ | ||
| 8 | """ | ||
| 9 | |||
| 10 | import os | ||
| 11 | |||
| 12 | from channels.auth import AuthMiddlewareStack | ||
| 13 | from channels.routing import ProtocolTypeRouter, URLRouter | ||
| 14 | from channels.security.websocket import AllowedHostsOriginValidator | ||
| 15 | from django.core.asgi import get_asgi_application | ||
| 16 | |||
| 17 | from chat.routing import websocket_urlpatterns | ||
| 18 | |||
| 19 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') | ||
| 20 | |||
| 21 | application = ProtocolTypeRouter( | ||
| 22 | { | ||
| 23 | "http": get_asgi_application(), | ||
| 24 | "websocket": AllowedHostsOriginValidator( | ||
| 25 | AuthMiddlewareStack(URLRouter(websocket_urlpatterns)) | ||
| 26 | ), | ||
| 27 | } | ||
| 28 | ) | ||
diff --git a/chat/consumers.py b/chat/consumers.py new file mode 100644 index 0000000..bfba381 --- /dev/null +++ b/chat/consumers.py | |||
| @@ -0,0 +1,65 @@ | |||
| 1 | import json | ||
| 2 | |||
| 3 | from asgiref.sync import async_to_sync | ||
| 4 | from channels.generic.websocket import WebsocketConsumer | ||
| 5 | |||
| 6 | |||
| 7 | class ChatConsumer(WebsocketConsumer): | ||
| 8 | def connect(self): | ||
| 9 | self.room_name = self.scope["url_route"]["kwargs"]["room_name"] | ||
| 10 | self.room_group_name = f"chat_{self.room_name}" | ||
| 11 | self.username = "Anonymous" | ||
| 12 | |||
| 13 | async_to_sync(self.channel_layer.group_add)( | ||
| 14 | self.room_group_name, self.channel_name | ||
| 15 | ) | ||
| 16 | self.accept() | ||
| 17 | |||
| 18 | def disconnect(self, close_code): | ||
| 19 | async_to_sync(self.channel_layer.group_discard)( | ||
| 20 | self.room_group_name, self.channel_name | ||
| 21 | ) | ||
| 22 | async_to_sync(self.channel_layer.group_send)( | ||
| 23 | self.room_group_name, | ||
| 24 | {"type": "system.message", "text": f"{self.username} left the chat"}, | ||
| 25 | ) | ||
| 26 | |||
| 27 | def receive(self, text_data): | ||
| 28 | text_data_json = json.loads(text_data) | ||
| 29 | msg_type = text_data_json.get("type", "message") | ||
| 30 | |||
| 31 | if msg_type == "join": | ||
| 32 | self.username = text_data_json.get("username", "Anonymous") | ||
| 33 | async_to_sync(self.channel_layer.group_send)( | ||
| 34 | self.room_group_name, | ||
| 35 | {"type": "system.message", "text": f"{self.username} joined the chat"}, | ||
| 36 | ) | ||
| 37 | return | ||
| 38 | |||
| 39 | if msg_type == "rename": | ||
| 40 | old_name = self.username | ||
| 41 | self.username = text_data_json.get("username", self.username) | ||
| 42 | async_to_sync(self.channel_layer.group_send)( | ||
| 43 | self.room_group_name, | ||
| 44 | {"type": "system.message", "text": f"{old_name} is now known as {self.username}"}, | ||
| 45 | ) | ||
| 46 | return | ||
| 47 | |||
| 48 | message = text_data_json["message"] | ||
| 49 | username = text_data_json.get("username", self.username) | ||
| 50 | async_to_sync(self.channel_layer.group_send)( | ||
| 51 | self.room_group_name, | ||
| 52 | {"type": "chat.message", "message": message, "username": username}, | ||
| 53 | ) | ||
| 54 | |||
| 55 | def chat_message(self, event): | ||
| 56 | self.send(text_data=json.dumps({ | ||
| 57 | "message": event["message"], | ||
| 58 | "username": event["username"], | ||
| 59 | })) | ||
| 60 | |||
| 61 | def system_message(self, event): | ||
| 62 | self.send(text_data=json.dumps({ | ||
| 63 | "type": "system", | ||
| 64 | "text": event["text"], | ||
| 65 | })) | ||
diff --git a/chat/routing.py b/chat/routing.py new file mode 100644 index 0000000..9f08ddf --- /dev/null +++ b/chat/routing.py | |||
| @@ -0,0 +1,7 @@ | |||
| 1 | from django.urls import re_path | ||
| 2 | |||
| 3 | from . import consumers | ||
| 4 | |||
| 5 | websocket_urlpatterns = [ | ||
| 6 | re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()), | ||
| 7 | ] | ||
diff --git a/chat/settings.py b/chat/settings.py new file mode 100644 index 0000000..653eea9 --- /dev/null +++ b/chat/settings.py | |||
| @@ -0,0 +1,135 @@ | |||
| 1 | """ | ||
| 2 | Django settings for chat project. | ||
| 3 | |||
| 4 | Generated by 'django-admin startproject' using Django 5.0.3. | ||
| 5 | |||
| 6 | For more information on this file, see | ||
| 7 | https://docs.djangoproject.com/en/5.0/topics/settings/ | ||
| 8 | |||
| 9 | For the full list of settings and their values, see | ||
| 10 | https://docs.djangoproject.com/en/5.0/ref/settings/ | ||
| 11 | """ | ||
| 12 | |||
| 13 | from pathlib import Path | ||
| 14 | |||
| 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. | ||
| 16 | BASE_DIR = Path(__file__).resolve().parent.parent | ||
| 17 | |||
| 18 | |||
| 19 | # Quick-start development settings - unsuitable for production | ||
| 20 | # See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/ | ||
| 21 | |||
| 22 | # SECURITY WARNING: keep the secret key used in production secret! | ||
| 23 | SECRET_KEY = 'django-insecure-dxn^kf6b8qm@#cqg4+jncop793g7*3mgok&p$40di_cxbm+y$3' | ||
| 24 | |||
| 25 | # SECURITY WARNING: don't run with debug turned on in production! | ||
| 26 | DEBUG = True | ||
| 27 | |||
| 28 | ALLOWED_HOSTS = ['*'] | ||
| 29 | |||
| 30 | |||
| 31 | # Application definition | ||
| 32 | |||
| 33 | INSTALLED_APPS = [ | ||
| 34 | 'chat', | ||
| 35 | 'daphne', | ||
| 36 | 'django.contrib.admin', | ||
| 37 | 'django.contrib.auth', | ||
| 38 | 'django.contrib.contenttypes', | ||
| 39 | 'django.contrib.sessions', | ||
| 40 | 'django.contrib.messages', | ||
| 41 | 'django.contrib.staticfiles', | ||
| 42 | ] | ||
| 43 | |||
| 44 | MIDDLEWARE = [ | ||
| 45 | 'django.middleware.security.SecurityMiddleware', | ||
| 46 | 'django.contrib.sessions.middleware.SessionMiddleware', | ||
| 47 | 'django.middleware.common.CommonMiddleware', | ||
| 48 | 'django.middleware.csrf.CsrfViewMiddleware', | ||
| 49 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | ||
| 50 | 'django.contrib.messages.middleware.MessageMiddleware', | ||
| 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', | ||
| 52 | ] | ||
| 53 | |||
| 54 | ROOT_URLCONF = 'chat.urls' | ||
| 55 | |||
| 56 | TEMPLATES = [ | ||
| 57 | { | ||
| 58 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | ||
| 59 | 'DIRS': [], | ||
| 60 | 'APP_DIRS': True, | ||
| 61 | 'OPTIONS': { | ||
| 62 | 'context_processors': [ | ||
| 63 | 'django.template.context_processors.debug', | ||
| 64 | 'django.template.context_processors.request', | ||
| 65 | 'django.contrib.auth.context_processors.auth', | ||
| 66 | 'django.contrib.messages.context_processors.messages', | ||
| 67 | ], | ||
| 68 | }, | ||
| 69 | }, | ||
| 70 | ] | ||
| 71 | |||
| 72 | WSGI_APPLICATION = 'chat.wsgi.application' | ||
| 73 | ASGI_APPLICATION = 'chat.asgi.application' | ||
| 74 | |||
| 75 | |||
| 76 | # Database | ||
| 77 | # https://docs.djangoproject.com/en/5.0/ref/settings/#databases | ||
| 78 | |||
| 79 | DATABASES = { | ||
| 80 | 'default': { | ||
| 81 | 'ENGINE': 'django.db.backends.sqlite3', | ||
| 82 | 'NAME': BASE_DIR / 'db.sqlite3', | ||
| 83 | } | ||
| 84 | } | ||
| 85 | |||
| 86 | |||
| 87 | # Password validation | ||
| 88 | # https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators | ||
| 89 | |||
| 90 | AUTH_PASSWORD_VALIDATORS = [ | ||
| 91 | { | ||
| 92 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', | ||
| 93 | }, | ||
| 94 | { | ||
| 95 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', | ||
| 96 | }, | ||
| 97 | { | ||
| 98 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', | ||
| 99 | }, | ||
| 100 | { | ||
| 101 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', | ||
| 102 | }, | ||
| 103 | ] | ||
| 104 | |||
| 105 | |||
| 106 | # Internationalization | ||
| 107 | # https://docs.djangoproject.com/en/5.0/topics/i18n/ | ||
| 108 | |||
| 109 | LANGUAGE_CODE = 'en-us' | ||
| 110 | |||
| 111 | TIME_ZONE = 'UTC' | ||
| 112 | |||
| 113 | USE_I18N = True | ||
| 114 | |||
| 115 | USE_TZ = True | ||
| 116 | |||
| 117 | |||
| 118 | # Static files (CSS, JavaScript, Images) | ||
| 119 | # https://docs.djangoproject.com/en/5.0/howto/static-files/ | ||
| 120 | |||
| 121 | STATIC_URL = 'static/' | ||
| 122 | |||
| 123 | # Default primary key field type | ||
| 124 | # https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field | ||
| 125 | |||
| 126 | DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' | ||
| 127 | |||
| 128 | CHANNEL_LAYERS = { | ||
| 129 | "default": { | ||
| 130 | "BACKEND": "channels_redis.core.RedisChannelLayer", | ||
| 131 | "CONFIG": { | ||
| 132 | "hosts": [("127.0.0.1", 6379)], | ||
| 133 | }, | ||
| 134 | }, | ||
| 135 | } | ||
diff --git a/chat/templates/chat.html b/chat/templates/chat.html new file mode 100644 index 0000000..8542368 --- /dev/null +++ b/chat/templates/chat.html | |||
| @@ -0,0 +1,136 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"/> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | ||
| 6 | <meta name="theme-color" content="#c9b8ff"> | ||
| 7 | <title>dreamchat</title> | ||
| 8 | <style> | ||
| 9 | :root { | ||
| 10 | --grad-start: #c9b8ff; | ||
| 11 | --grad-end: #f9b8e8; | ||
| 12 | --accent: #8b6cef; | ||
| 13 | --surface: #ffffff; | ||
| 14 | --text-main: #2d2040; | ||
| 15 | --text-muted: #8c7faa; | ||
| 16 | --radius-card: 24px; | ||
| 17 | --shadow: 0 8px 32px rgba(139, 108, 239, 0.18); | ||
| 18 | --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| 19 | } | ||
| 20 | |||
| 21 | * { box-sizing: border-box; margin: 0; padding: 0; } | ||
| 22 | |||
| 23 | body { | ||
| 24 | min-height: 100dvh; | ||
| 25 | background: linear-gradient(135deg, var(--grad-start), var(--grad-end)); | ||
| 26 | display: flex; | ||
| 27 | align-items: center; | ||
| 28 | justify-content: center; | ||
| 29 | font-family: var(--font); | ||
| 30 | padding: 24px; | ||
| 31 | } | ||
| 32 | |||
| 33 | .card { | ||
| 34 | background: var(--surface); | ||
| 35 | border-radius: var(--radius-card); | ||
| 36 | box-shadow: var(--shadow); | ||
| 37 | padding: 40px 32px; | ||
| 38 | width: 100%; | ||
| 39 | max-width: 400px; | ||
| 40 | text-align: center; | ||
| 41 | } | ||
| 42 | |||
| 43 | .logo { | ||
| 44 | font-size: 3rem; | ||
| 45 | margin-bottom: 12px; | ||
| 46 | line-height: 1; | ||
| 47 | } | ||
| 48 | |||
| 49 | .title { | ||
| 50 | font-size: 2rem; | ||
| 51 | font-weight: 700; | ||
| 52 | background: linear-gradient(135deg, var(--accent), #e06aba); | ||
| 53 | -webkit-background-clip: text; | ||
| 54 | -webkit-text-fill-color: transparent; | ||
| 55 | background-clip: text; | ||
| 56 | margin-bottom: 8px; | ||
| 57 | } | ||
| 58 | |||
| 59 | .subtitle { | ||
| 60 | color: var(--text-muted); | ||
| 61 | margin-bottom: 28px; | ||
| 62 | font-size: 0.95rem; | ||
| 63 | } | ||
| 64 | |||
| 65 | .input-row { | ||
| 66 | display: flex; | ||
| 67 | gap: 10px; | ||
| 68 | } | ||
| 69 | |||
| 70 | .input-row input { | ||
| 71 | flex: 1; | ||
| 72 | min-width: 0; | ||
| 73 | border: 2px solid #e8e0ff; | ||
| 74 | border-radius: 50px; | ||
| 75 | padding: 12px 18px; | ||
| 76 | font-size: 1rem; | ||
| 77 | font-family: var(--font); | ||
| 78 | color: var(--text-main); | ||
| 79 | outline: none; | ||
| 80 | transition: border-color 0.2s; | ||
| 81 | } | ||
| 82 | |||
| 83 | .input-row input:focus { | ||
| 84 | border-color: var(--accent); | ||
| 85 | } | ||
| 86 | |||
| 87 | .input-row input::placeholder { | ||
| 88 | color: var(--text-muted); | ||
| 89 | } | ||
| 90 | |||
| 91 | .input-row button { | ||
| 92 | background: linear-gradient(135deg, var(--accent), #b06add); | ||
| 93 | color: #fff; | ||
| 94 | border: none; | ||
| 95 | border-radius: 50px; | ||
| 96 | padding: 12px 22px; | ||
| 97 | font-size: 1rem; | ||
| 98 | font-weight: 600; | ||
| 99 | font-family: var(--font); | ||
| 100 | cursor: pointer; | ||
| 101 | white-space: nowrap; | ||
| 102 | transition: opacity 0.15s, transform 0.1s; | ||
| 103 | } | ||
| 104 | |||
| 105 | .input-row button:hover { opacity: 0.9; } | ||
| 106 | .input-row button:active { transform: scale(0.96); } | ||
| 107 | </style> | ||
| 108 | </head> | ||
| 109 | <body> | ||
| 110 | <div class="card"> | ||
| 111 | <div class="logo">💬</div> | ||
| 112 | <h1 class="title">dreamchat</h1> | ||
| 113 | <p class="subtitle">Where do you want to go?</p> | ||
| 114 | <div class="input-row"> | ||
| 115 | <input id="room-name-input" type="text" placeholder="Room name" | ||
| 116 | aria-label="Room name" | ||
| 117 | autocomplete="off" autocapitalize="off" spellcheck="false"> | ||
| 118 | <button id="room-name-submit">Enter</button> | ||
| 119 | </div> | ||
| 120 | </div> | ||
| 121 | |||
| 122 | <script> | ||
| 123 | document.querySelector('#room-name-input').focus(); | ||
| 124 | document.querySelector('#room-name-input').onkeyup = function(event) { | ||
| 125 | if (event.key === 'Enter') { | ||
| 126 | document.querySelector('#room-name-submit').click(); | ||
| 127 | } | ||
| 128 | }; | ||
| 129 | |||
| 130 | document.querySelector('#room-name-submit').onclick = function() { | ||
| 131 | const roomName = document.querySelector('#room-name-input').value; | ||
| 132 | window.location.pathname = '/' + roomName + '/'; | ||
| 133 | }; | ||
| 134 | </script> | ||
| 135 | </body> | ||
| 136 | </html> | ||
diff --git a/chat/templates/room.html b/chat/templates/room.html new file mode 100644 index 0000000..de76f67 --- /dev/null +++ b/chat/templates/room.html | |||
| @@ -0,0 +1,451 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html lang="en"> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"/> | ||
| 5 | <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> | ||
| 6 | <meta name="theme-color" content="#c9b8ff"> | ||
| 7 | <title>{{ room_name }} — dreamchat</title> | ||
| 8 | <style> | ||
| 9 | :root { | ||
| 10 | --grad-start: #c9b8ff; | ||
| 11 | --grad-end: #f9b8e8; | ||
| 12 | --accent: #8b6cef; | ||
| 13 | --bubble-in-bg: #f0ebff; | ||
| 14 | --bubble-in-text: #2d2040; | ||
| 15 | --surface: #ffffff; | ||
| 16 | --text-main: #2d2040; | ||
| 17 | --text-muted: #8c7faa; | ||
| 18 | --radius-bubble: 18px; | ||
| 19 | --font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; | ||
| 20 | } | ||
| 21 | |||
| 22 | * { box-sizing: border-box; margin: 0; padding: 0; } | ||
| 23 | |||
| 24 | body { | ||
| 25 | height: 100dvh; | ||
| 26 | display: flex; | ||
| 27 | flex-direction: column; | ||
| 28 | background: linear-gradient(160deg, #ede7ff 0%, #fce4f6 100%); | ||
| 29 | font-family: var(--font); | ||
| 30 | overflow: hidden; | ||
| 31 | } | ||
| 32 | |||
| 33 | /* ---- Header ---- */ | ||
| 34 | .room-header { | ||
| 35 | display: flex; | ||
| 36 | align-items: center; | ||
| 37 | gap: 12px; | ||
| 38 | padding: 10px 16px; | ||
| 39 | padding-top: calc(10px + env(safe-area-inset-top)); | ||
| 40 | background: rgba(255, 255, 255, 0.75); | ||
| 41 | backdrop-filter: blur(12px); | ||
| 42 | -webkit-backdrop-filter: blur(12px); | ||
| 43 | border-bottom: 1px solid rgba(139, 108, 239, 0.12); | ||
| 44 | flex-shrink: 0; | ||
| 45 | } | ||
| 46 | |||
| 47 | .back-btn { | ||
| 48 | display: flex; | ||
| 49 | align-items: center; | ||
| 50 | justify-content: center; | ||
| 51 | width: 32px; | ||
| 52 | height: 32px; | ||
| 53 | border-radius: 50%; | ||
| 54 | color: var(--accent); | ||
| 55 | text-decoration: none; | ||
| 56 | font-size: 1.2rem; | ||
| 57 | font-weight: 600; | ||
| 58 | transition: background 0.15s; | ||
| 59 | flex-shrink: 0; | ||
| 60 | } | ||
| 61 | |||
| 62 | .back-btn:hover { background: #f0ebff; } | ||
| 63 | |||
| 64 | .header-center { | ||
| 65 | flex: 1; | ||
| 66 | min-width: 0; | ||
| 67 | display: flex; | ||
| 68 | flex-direction: column; | ||
| 69 | gap: 2px; | ||
| 70 | } | ||
| 71 | |||
| 72 | .room-title { | ||
| 73 | font-size: 1.05rem; | ||
| 74 | font-weight: 700; | ||
| 75 | background: linear-gradient(135deg, var(--accent), #e06aba); | ||
| 76 | -webkit-background-clip: text; | ||
| 77 | -webkit-text-fill-color: transparent; | ||
| 78 | background-clip: text; | ||
| 79 | white-space: nowrap; | ||
| 80 | overflow: hidden; | ||
| 81 | text-overflow: ellipsis; | ||
| 82 | } | ||
| 83 | |||
| 84 | .username-row { display: flex; align-items: center; } | ||
| 85 | |||
| 86 | .username-chip { | ||
| 87 | display: flex; | ||
| 88 | align-items: center; | ||
| 89 | gap: 4px; | ||
| 90 | background: none; | ||
| 91 | border: none; | ||
| 92 | padding: 0; | ||
| 93 | font-size: 0.78rem; | ||
| 94 | font-family: var(--font); | ||
| 95 | color: var(--text-muted); | ||
| 96 | cursor: pointer; | ||
| 97 | transition: color 0.15s; | ||
| 98 | } | ||
| 99 | |||
| 100 | .username-chip:hover { color: var(--accent); } | ||
| 101 | .edit-icon { font-size: 0.7rem; } | ||
| 102 | |||
| 103 | .username-edit { | ||
| 104 | font-size: 0.78rem; | ||
| 105 | font-family: var(--font); | ||
| 106 | color: var(--text-main); | ||
| 107 | border: none; | ||
| 108 | border-bottom: 1.5px solid var(--accent); | ||
| 109 | outline: none; | ||
| 110 | background: transparent; | ||
| 111 | width: 130px; | ||
| 112 | padding: 1px 2px; | ||
| 113 | } | ||
| 114 | |||
| 115 | .disconnect-btn { | ||
| 116 | display: flex; | ||
| 117 | align-items: center; | ||
| 118 | justify-content: center; | ||
| 119 | width: 32px; | ||
| 120 | height: 32px; | ||
| 121 | background: none; | ||
| 122 | border: 1.5px solid #d0c5f0; | ||
| 123 | color: var(--text-muted); | ||
| 124 | border-radius: 50%; | ||
| 125 | font-size: 0.85rem; | ||
| 126 | cursor: pointer; | ||
| 127 | flex-shrink: 0; | ||
| 128 | transition: background 0.15s, border-color 0.15s; | ||
| 129 | } | ||
| 130 | |||
| 131 | .disconnect-btn:hover { background: #f0ebff; border-color: var(--accent); } | ||
| 132 | |||
| 133 | /* ---- Message area ---- */ | ||
| 134 | .chat-log { | ||
| 135 | flex: 1; | ||
| 136 | overflow-y: auto; | ||
| 137 | -webkit-overflow-scrolling: touch; | ||
| 138 | padding: 16px 12px; | ||
| 139 | display: flex; | ||
| 140 | flex-direction: column; | ||
| 141 | gap: 6px; | ||
| 142 | } | ||
| 143 | |||
| 144 | /* ---- Bubbles ---- */ | ||
| 145 | .bubble-row { | ||
| 146 | display: flex; | ||
| 147 | width: 100%; | ||
| 148 | } | ||
| 149 | |||
| 150 | .bubble-row.sent { justify-content: flex-end; } | ||
| 151 | .bubble-row.recv { justify-content: flex-start; } | ||
| 152 | |||
| 153 | .bubble-wrapper { | ||
| 154 | display: flex; | ||
| 155 | flex-direction: column; | ||
| 156 | max-width: 72%; | ||
| 157 | gap: 3px; | ||
| 158 | } | ||
| 159 | |||
| 160 | .bubble-row.sent .bubble-wrapper { align-items: flex-end; } | ||
| 161 | .bubble-row.recv .bubble-wrapper { align-items: flex-start; } | ||
| 162 | |||
| 163 | .bubble { | ||
| 164 | padding: 10px 15px; | ||
| 165 | border-radius: var(--radius-bubble); | ||
| 166 | font-size: 0.97rem; | ||
| 167 | line-height: 1.45; | ||
| 168 | word-wrap: break-word; | ||
| 169 | animation: pop-in 0.15s ease-out; | ||
| 170 | } | ||
| 171 | |||
| 172 | .sent .bubble { | ||
| 173 | background: linear-gradient(135deg, var(--accent), #b06add); | ||
| 174 | color: #ffffff; | ||
| 175 | border-bottom-right-radius: 4px; | ||
| 176 | } | ||
| 177 | |||
| 178 | .recv .bubble { | ||
| 179 | background: var(--bubble-in-bg); | ||
| 180 | color: var(--bubble-in-text); | ||
| 181 | border-bottom-left-radius: 4px; | ||
| 182 | box-shadow: 0 2px 8px rgba(139, 108, 239, 0.1); | ||
| 183 | } | ||
| 184 | |||
| 185 | .bubble-sender { | ||
| 186 | font-size: 0.75rem; | ||
| 187 | font-weight: 600; | ||
| 188 | color: var(--accent); | ||
| 189 | padding: 0 4px; | ||
| 190 | } | ||
| 191 | |||
| 192 | .bubble-time { | ||
| 193 | font-size: 0.7rem; | ||
| 194 | color: var(--text-muted); | ||
| 195 | padding: 0 4px; | ||
| 196 | } | ||
| 197 | |||
| 198 | @keyframes pop-in { | ||
| 199 | from { opacity: 0; transform: scale(0.9) translateY(4px); } | ||
| 200 | to { opacity: 1; transform: scale(1) translateY(0); } | ||
| 201 | } | ||
| 202 | |||
| 203 | /* ---- Input bar ---- */ | ||
| 204 | .input-bar { | ||
| 205 | display: flex; | ||
| 206 | align-items: center; | ||
| 207 | gap: 10px; | ||
| 208 | padding: 12px 16px; | ||
| 209 | padding-bottom: calc(12px + env(safe-area-inset-bottom)); | ||
| 210 | background: rgba(255, 255, 255, 0.85); | ||
| 211 | backdrop-filter: blur(12px); | ||
| 212 | -webkit-backdrop-filter: blur(12px); | ||
| 213 | border-top: 1px solid rgba(139, 108, 239, 0.12); | ||
| 214 | flex-shrink: 0; | ||
| 215 | } | ||
| 216 | |||
| 217 | .input-bar input { | ||
| 218 | flex: 1; | ||
| 219 | min-width: 0; | ||
| 220 | border: 2px solid #e8e0ff; | ||
| 221 | border-radius: 50px; | ||
| 222 | padding: 11px 18px; | ||
| 223 | font-size: 1rem; | ||
| 224 | font-family: var(--font); | ||
| 225 | color: var(--text-main); | ||
| 226 | background: #fff; | ||
| 227 | outline: none; | ||
| 228 | transition: border-color 0.2s; | ||
| 229 | } | ||
| 230 | |||
| 231 | .input-bar input:focus { border-color: var(--accent); } | ||
| 232 | .input-bar input::placeholder { color: var(--text-muted); } | ||
| 233 | |||
| 234 | .send-btn { | ||
| 235 | display: flex; | ||
| 236 | align-items: center; | ||
| 237 | justify-content: center; | ||
| 238 | width: 44px; | ||
| 239 | height: 44px; | ||
| 240 | background: linear-gradient(135deg, var(--accent), #b06add); | ||
| 241 | color: #fff; | ||
| 242 | border: none; | ||
| 243 | border-radius: 50%; | ||
| 244 | font-size: 1rem; | ||
| 245 | cursor: pointer; | ||
| 246 | flex-shrink: 0; | ||
| 247 | transition: opacity 0.15s, transform 0.1s; | ||
| 248 | } | ||
| 249 | |||
| 250 | .send-btn:hover { opacity: 0.9; } | ||
| 251 | .send-btn:active { transform: scale(0.93); } | ||
| 252 | |||
| 253 | /* ---- System messages ---- */ | ||
| 254 | .system-msg { | ||
| 255 | display: flex; | ||
| 256 | align-items: center; | ||
| 257 | gap: 10px; | ||
| 258 | font-size: 0.72rem; | ||
| 259 | color: var(--text-muted); | ||
| 260 | padding: 4px 0; | ||
| 261 | animation: pop-in 0.2s ease-out; | ||
| 262 | } | ||
| 263 | |||
| 264 | .system-msg::before, | ||
| 265 | .system-msg::after { | ||
| 266 | content: ''; | ||
| 267 | flex: 1; | ||
| 268 | height: 1px; | ||
| 269 | background: rgba(139, 108, 239, 0.2); | ||
| 270 | } | ||
| 271 | </style> | ||
| 272 | </head> | ||
| 273 | <body> | ||
| 274 | <header class="room-header"> | ||
| 275 | <a href="/" class="back-btn" aria-label="Back to rooms">←</a> | ||
| 276 | <div class="header-center"> | ||
| 277 | <span class="room-title">{{ room_name }}</span> | ||
| 278 | <div class="username-row"> | ||
| 279 | <button id="username-display" class="username-chip" aria-label="Edit username"> | ||
| 280 | <span id="username-text"></span> | ||
| 281 | <span class="edit-icon">✏️</span> | ||
| 282 | </button> | ||
| 283 | <input id="username-input" class="username-edit" | ||
| 284 | aria-label="Your username" hidden | ||
| 285 | maxlength="30" autocomplete="off" spellcheck="false"> | ||
| 286 | </div> | ||
| 287 | </div> | ||
| 288 | <button id="chat-message-disconnect" class="disconnect-btn" hidden aria-label="Disconnect">✕</button> | ||
| 289 | </header> | ||
| 290 | |||
| 291 | <main id="chat-log" class="chat-log" aria-live="polite" aria-label="Chat messages"></main> | ||
| 292 | |||
| 293 | <footer class="input-bar"> | ||
| 294 | <input id="chat-message-input" type="text" placeholder="Message…" | ||
| 295 | aria-label="Message" | ||
| 296 | autocomplete="off" autocapitalize="sentences"> | ||
| 297 | <button id="chat-message-submit" class="send-btn" aria-label="Send">➤</button> | ||
| 298 | </footer> | ||
| 299 | |||
| 300 | {{ room_name|json_script:"room-name" }} | ||
| 301 | <script> | ||
| 302 | const ADJECTIVES = ['Dreamy','Cosmic','Fuzzy','Glowing','Misty','Silky','Starry','Velvet','Cozy','Dewy','Wispy','Breezy']; | ||
| 303 | const ANIMALS = ['Fox','Moth','Owl','Bear','Deer','Lark','Lynx','Mink','Swan','Wolf','Wren','Panda']; | ||
| 304 | |||
| 305 | function generateUsername() { | ||
| 306 | const adj = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; | ||
| 307 | const animal = ANIMALS[Math.floor(Math.random() * ANIMALS.length)]; | ||
| 308 | return adj + animal; | ||
| 309 | } | ||
| 310 | |||
| 311 | let username = localStorage.getItem('dreamchat_username') || generateUsername(); | ||
| 312 | localStorage.setItem('dreamchat_username', username); | ||
| 313 | document.getElementById('username-text').textContent = username; | ||
| 314 | |||
| 315 | document.getElementById('username-display').onclick = function() { | ||
| 316 | const input = document.getElementById('username-input'); | ||
| 317 | input.value = username; | ||
| 318 | input.removeAttribute('hidden'); | ||
| 319 | this.setAttribute('hidden', ''); | ||
| 320 | input.focus(); | ||
| 321 | input.select(); | ||
| 322 | }; | ||
| 323 | |||
| 324 | function commitUsername() { | ||
| 325 | const input = document.getElementById('username-input'); | ||
| 326 | const newName = input.value.trim(); | ||
| 327 | if (newName && newName !== username) { | ||
| 328 | username = newName; | ||
| 329 | localStorage.setItem('dreamchat_username', username); | ||
| 330 | document.getElementById('username-text').textContent = username; | ||
| 331 | if (chatSocket.readyState === WebSocket.OPEN) { | ||
| 332 | chatSocket.send(JSON.stringify({ type: 'rename', username })); | ||
| 333 | } | ||
| 334 | } | ||
| 335 | input.setAttribute('hidden', ''); | ||
| 336 | document.getElementById('username-display').removeAttribute('hidden'); | ||
| 337 | } | ||
| 338 | |||
| 339 | document.getElementById('username-input').onblur = commitUsername; | ||
| 340 | document.getElementById('username-input').onkeyup = function(event) { | ||
| 341 | if (event.key === 'Enter') { this.blur(); } | ||
| 342 | if (event.key === 'Escape') { | ||
| 343 | this.setAttribute('hidden', ''); | ||
| 344 | document.getElementById('username-display').removeAttribute('hidden'); | ||
| 345 | } | ||
| 346 | }; | ||
| 347 | |||
| 348 | function formatTime(date) { | ||
| 349 | return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | ||
| 350 | } | ||
| 351 | |||
| 352 | const roomName = JSON.parse(document.getElementById('room-name').textContent); | ||
| 353 | const chatLog = document.getElementById('chat-log'); | ||
| 354 | const pendingEchoes = new Set(); | ||
| 355 | |||
| 356 | function appendSystemMessage(text) { | ||
| 357 | const el = document.createElement('div'); | ||
| 358 | el.className = 'system-msg'; | ||
| 359 | el.textContent = text; | ||
| 360 | chatLog.appendChild(el); | ||
| 361 | chatLog.scrollTop = chatLog.scrollHeight; | ||
| 362 | } | ||
| 363 | |||
| 364 | function appendBubble(text, direction, sender, time) { | ||
| 365 | const row = document.createElement('div'); | ||
| 366 | row.className = 'bubble-row ' + direction; | ||
| 367 | |||
| 368 | const wrapper = document.createElement('div'); | ||
| 369 | wrapper.className = 'bubble-wrapper'; | ||
| 370 | |||
| 371 | if (direction === 'recv') { | ||
| 372 | const nameEl = document.createElement('div'); | ||
| 373 | nameEl.className = 'bubble-sender'; | ||
| 374 | nameEl.textContent = sender; | ||
| 375 | wrapper.appendChild(nameEl); | ||
| 376 | } | ||
| 377 | |||
| 378 | const bubble = document.createElement('div'); | ||
| 379 | bubble.className = 'bubble'; | ||
| 380 | bubble.textContent = text; | ||
| 381 | wrapper.appendChild(bubble); | ||
| 382 | |||
| 383 | const timeEl = document.createElement('div'); | ||
| 384 | timeEl.className = 'bubble-time'; | ||
| 385 | timeEl.textContent = time; | ||
| 386 | wrapper.appendChild(timeEl); | ||
| 387 | |||
| 388 | row.appendChild(wrapper); | ||
| 389 | chatLog.appendChild(row); | ||
| 390 | chatLog.scrollTop = chatLog.scrollHeight; | ||
| 391 | } | ||
| 392 | |||
| 393 | const chatSocket = new WebSocket( | ||
| 394 | 'ws://' | ||
| 395 | + window.location.host | ||
| 396 | + '/ws/chat/' | ||
| 397 | + roomName | ||
| 398 | + '/' | ||
| 399 | + '?token=123' | ||
| 400 | ); | ||
| 401 | |||
| 402 | chatSocket.onopen = function() { | ||
| 403 | document.querySelector('#chat-message-disconnect').removeAttribute('hidden'); | ||
| 404 | chatSocket.send(JSON.stringify({ type: 'join', username })); | ||
| 405 | }; | ||
| 406 | |||
| 407 | chatSocket.onmessage = function(e) { | ||
| 408 | const data = JSON.parse(e.data); | ||
| 409 | |||
| 410 | if (data.type === 'system') { | ||
| 411 | appendSystemMessage(data.text); | ||
| 412 | return; | ||
| 413 | } | ||
| 414 | |||
| 415 | const key = data.username + '|' + data.message; | ||
| 416 | if (pendingEchoes.has(key)) { | ||
| 417 | pendingEchoes.delete(key); | ||
| 418 | return; | ||
| 419 | } | ||
| 420 | appendBubble(data.message, 'recv', data.username, formatTime(new Date())); | ||
| 421 | }; | ||
| 422 | |||
| 423 | chatSocket.onclose = function() { | ||
| 424 | console.error('Chat socket closed unexpectedly'); | ||
| 425 | document.querySelector('#chat-message-disconnect').setAttribute('hidden', ''); | ||
| 426 | }; | ||
| 427 | |||
| 428 | document.querySelector('#chat-message-input').focus(); | ||
| 429 | document.querySelector('#chat-message-input').onkeyup = function(event) { | ||
| 430 | if (event.key === 'Enter') { | ||
| 431 | document.querySelector('#chat-message-submit').click(); | ||
| 432 | } | ||
| 433 | }; | ||
| 434 | |||
| 435 | document.querySelector('#chat-message-submit').onclick = function() { | ||
| 436 | const messageInputDom = document.querySelector('#chat-message-input'); | ||
| 437 | const message = messageInputDom.value.trim(); | ||
| 438 | if (!message) return; | ||
| 439 | const time = formatTime(new Date()); | ||
| 440 | pendingEchoes.add(username + '|' + message); | ||
| 441 | appendBubble(message, 'sent', username, time); | ||
| 442 | chatSocket.send(JSON.stringify({ message, username })); | ||
| 443 | messageInputDom.value = ''; | ||
| 444 | }; | ||
| 445 | |||
| 446 | document.querySelector('#chat-message-disconnect').onclick = function() { | ||
| 447 | chatSocket.close(); | ||
| 448 | }; | ||
| 449 | </script> | ||
| 450 | </body> | ||
| 451 | </html> | ||
diff --git a/chat/urls.py b/chat/urls.py new file mode 100644 index 0000000..b96b107 --- /dev/null +++ b/chat/urls.py | |||
| @@ -0,0 +1,26 @@ | |||
| 1 | """ | ||
| 2 | URL configuration for chat project. | ||
| 3 | |||
| 4 | The `urlpatterns` list routes URLs to views. For more information please see: | ||
| 5 | https://docs.djangoproject.com/en/5.0/topics/http/urls/ | ||
| 6 | Examples: | ||
| 7 | Function views | ||
| 8 | 1. Add an import: from my_app import views | ||
| 9 | 2. Add a URL to urlpatterns: path('', views.home, name='home') | ||
| 10 | Class-based views | ||
| 11 | 1. Add an import: from other_app.views import Home | ||
| 12 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') | ||
| 13 | Including another URLconf | ||
| 14 | 1. Import the include() function: from django.urls import include, path | ||
| 15 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) | ||
| 16 | """ | ||
| 17 | from django.contrib import admin | ||
| 18 | from django.urls import path | ||
| 19 | |||
| 20 | from chat.views import Chat, Room | ||
| 21 | |||
| 22 | urlpatterns = [ | ||
| 23 | path('admin/', admin.site.urls), | ||
| 24 | path('<str:room>/', Room.as_view()), | ||
| 25 | path('', Chat.as_view()), | ||
| 26 | ] | ||
diff --git a/chat/views.py b/chat/views.py new file mode 100644 index 0000000..512837e --- /dev/null +++ b/chat/views.py | |||
| @@ -0,0 +1,14 @@ | |||
| 1 | from django.views.generic import TemplateView | ||
| 2 | |||
| 3 | |||
| 4 | class Chat(TemplateView): | ||
| 5 | template_name = 'chat.html' | ||
| 6 | |||
| 7 | class Room(TemplateView): | ||
| 8 | template_name = 'room.html' | ||
| 9 | |||
| 10 | def get_context_data(self, **kwargs): | ||
| 11 | return { | ||
| 12 | **super().get_context_data(**kwargs), | ||
| 13 | 'room_name': kwargs.get('room'), | ||
| 14 | } | ||
diff --git a/chat/wsgi.py b/chat/wsgi.py new file mode 100644 index 0000000..cdfbb23 --- /dev/null +++ b/chat/wsgi.py | |||
| @@ -0,0 +1,16 @@ | |||
| 1 | """ | ||
| 2 | WSGI config for chat project. | ||
| 3 | |||
| 4 | It exposes the WSGI callable as a module-level variable named ``application``. | ||
| 5 | |||
| 6 | For more information on this file, see | ||
| 7 | https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ | ||
| 8 | """ | ||
| 9 | |||
| 10 | import os | ||
| 11 | |||
| 12 | from django.core.wsgi import get_wsgi_application | ||
| 13 | |||
| 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') | ||
| 15 | |||
| 16 | application = get_wsgi_application() | ||
diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..60eb248 --- /dev/null +++ b/manage.py | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | #!/usr/bin/env python | ||
| 2 | """Django's command-line utility for administrative tasks.""" | ||
| 3 | import os | ||
| 4 | import sys | ||
| 5 | |||
| 6 | |||
| 7 | def main(): | ||
| 8 | """Run administrative tasks.""" | ||
| 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'chat.settings') | ||
| 10 | try: | ||
| 11 | from django.core.management import execute_from_command_line | ||
| 12 | except ImportError as exc: | ||
| 13 | raise ImportError( | ||
| 14 | "Couldn't import Django. Are you sure it's installed and " | ||
| 15 | "available on your PYTHONPATH environment variable? Did you " | ||
| 16 | "forget to activate a virtual environment?" | ||
| 17 | ) from exc | ||
| 18 | execute_from_command_line(sys.argv) | ||
| 19 | |||
| 20 | |||
| 21 | if __name__ == '__main__': | ||
| 22 | main() | ||
diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..d13bf24 --- /dev/null +++ b/pyproject.toml | |||
| @@ -0,0 +1,11 @@ | |||
| 1 | [project] | ||
| 2 | name = "websocket-chat" | ||
| 3 | version = "0.1.0" | ||
| 4 | description = "Add your description here" | ||
| 5 | readme = "README.md" | ||
| 6 | requires-python = ">=3.13" | ||
| 7 | dependencies = [ | ||
| 8 | "channels-redis>=4.3.0", | ||
| 9 | "channels[daphne]>=4.3.1", | ||
| 10 | "django>=5.2.5", | ||
| 11 | ] | ||
diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..46e2756 --- /dev/null +++ b/requirements.txt | |||
| @@ -0,0 +1,3 @@ | |||
| 1 | Django | ||
| 2 | channels[daphne] | ||
| 3 | channels_redis | ||
| @@ -0,0 +1,393 @@ | |||
| 1 | version = 1 | ||
| 2 | revision = 1 | ||
| 3 | requires-python = ">=3.13" | ||
| 4 | |||
| 5 | [[package]] | ||
| 6 | name = "asgiref" | ||
| 7 | version = "3.9.1" | ||
| 8 | source = { registry = "https://pypi.org/simple" } | ||
| 9 | sdist = { url = "https://files.pythonhosted.org/packages/90/61/0aa957eec22ff70b830b22ff91f825e70e1ef732c06666a805730f28b36b/asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142", size = 36870 } | ||
| 10 | wheels = [ | ||
| 11 | { url = "https://files.pythonhosted.org/packages/7c/3c/0464dcada90d5da0e71018c04a140ad6349558afb30b3051b4264cc5b965/asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c", size = 23790 }, | ||
| 12 | ] | ||
| 13 | |||
| 14 | [[package]] | ||
| 15 | name = "attrs" | ||
| 16 | version = "25.3.0" | ||
| 17 | source = { registry = "https://pypi.org/simple" } | ||
| 18 | sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 } | ||
| 19 | wheels = [ | ||
| 20 | { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 }, | ||
| 21 | ] | ||
| 22 | |||
| 23 | [[package]] | ||
| 24 | name = "autobahn" | ||
| 25 | version = "24.4.2" | ||
| 26 | source = { registry = "https://pypi.org/simple" } | ||
| 27 | dependencies = [ | ||
| 28 | { name = "cryptography" }, | ||
| 29 | { name = "hyperlink" }, | ||
| 30 | { name = "setuptools" }, | ||
| 31 | { name = "txaio" }, | ||
| 32 | ] | ||
| 33 | sdist = { url = "https://files.pythonhosted.org/packages/38/f2/8dffb3b709383ba5b47628b0cc4e43e8d12d59eecbddb62cfccac2e7cf6a/autobahn-24.4.2.tar.gz", hash = "sha256:a2d71ef1b0cf780b6d11f8b205fd2c7749765e65795f2ea7d823796642ee92c9", size = 482700 } | ||
| 34 | wheels = [ | ||
| 35 | { url = "https://files.pythonhosted.org/packages/13/ee/a6475f39ef6c6f41c33da6b193e0ffd2c6048f52e1698be6253c59301b72/autobahn-24.4.2-py2.py3-none-any.whl", hash = "sha256:c56a2abe7ac78abbfb778c02892d673a4de58fd004d088cd7ab297db25918e81", size = 666965 }, | ||
| 36 | ] | ||
| 37 | |||
| 38 | [[package]] | ||
| 39 | name = "automat" | ||
| 40 | version = "25.4.16" | ||
| 41 | source = { registry = "https://pypi.org/simple" } | ||
| 42 | sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977 } | ||
| 43 | wheels = [ | ||
| 44 | { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842 }, | ||
| 45 | ] | ||
| 46 | |||
| 47 | [[package]] | ||
| 48 | name = "cffi" | ||
| 49 | version = "1.17.1" | ||
| 50 | source = { registry = "https://pypi.org/simple" } | ||
| 51 | dependencies = [ | ||
| 52 | { name = "pycparser" }, | ||
| 53 | ] | ||
| 54 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } | ||
| 55 | wheels = [ | ||
| 56 | { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 }, | ||
| 57 | { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 }, | ||
| 58 | { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 }, | ||
| 59 | { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 }, | ||
| 60 | { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 }, | ||
| 61 | { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 }, | ||
| 62 | { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 }, | ||
| 63 | { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 }, | ||
| 64 | { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 }, | ||
| 65 | { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 }, | ||
| 66 | { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 }, | ||
| 67 | ] | ||
| 68 | |||
| 69 | [[package]] | ||
| 70 | name = "channels" | ||
| 71 | version = "4.3.1" | ||
| 72 | source = { registry = "https://pypi.org/simple" } | ||
| 73 | dependencies = [ | ||
| 74 | { name = "asgiref" }, | ||
| 75 | { name = "django" }, | ||
| 76 | ] | ||
| 77 | sdist = { url = "https://files.pythonhosted.org/packages/12/a0/46450fcf9e56af18a6b0440ba49db6635419bb7bc84142c35f4143b1a66c/channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb", size = 26896 } | ||
| 78 | wheels = [ | ||
| 79 | { url = "https://files.pythonhosted.org/packages/89/1c/eae1c2a8c195760376e7f65d0bdcc3e966695d29cfbe5c54841ce5c71408/channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859", size = 31286 }, | ||
| 80 | ] | ||
| 81 | |||
| 82 | [package.optional-dependencies] | ||
| 83 | daphne = [ | ||
| 84 | { name = "daphne" }, | ||
| 85 | ] | ||
| 86 | |||
| 87 | [[package]] | ||
| 88 | name = "channels-redis" | ||
| 89 | version = "4.3.0" | ||
| 90 | source = { registry = "https://pypi.org/simple" } | ||
| 91 | dependencies = [ | ||
| 92 | { name = "asgiref" }, | ||
| 93 | { name = "channels" }, | ||
| 94 | { name = "msgpack" }, | ||
| 95 | { name = "redis" }, | ||
| 96 | ] | ||
| 97 | sdist = { url = "https://files.pythonhosted.org/packages/ab/69/fd3407ad407a80e72ca53850eb7a4c306273e67d5bbb71a86d0e6d088439/channels_redis-4.3.0.tar.gz", hash = "sha256:740ee7b54f0e28cf2264a940a24453d3f00526a96931f911fcb69228ef245dd2", size = 31440 } | ||
| 98 | wheels = [ | ||
| 99 | { url = "https://files.pythonhosted.org/packages/df/fe/b7224a401ad227b263e5ba84753ffb5a88df048f3b15efd2797903543ce4/channels_redis-4.3.0-py3-none-any.whl", hash = "sha256:48f3e902ae2d5fef7080215524f3b4a1d3cea4e304150678f867a1a822c0d9f5", size = 20641 }, | ||
| 100 | ] | ||
| 101 | |||
| 102 | [[package]] | ||
| 103 | name = "constantly" | ||
| 104 | version = "23.10.4" | ||
| 105 | source = { registry = "https://pypi.org/simple" } | ||
| 106 | sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300 } | ||
| 107 | wheels = [ | ||
| 108 | { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547 }, | ||
| 109 | ] | ||
| 110 | |||
| 111 | [[package]] | ||
| 112 | name = "cryptography" | ||
| 113 | version = "45.0.7" | ||
| 114 | source = { registry = "https://pypi.org/simple" } | ||
| 115 | dependencies = [ | ||
| 116 | { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, | ||
| 117 | ] | ||
| 118 | sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980 } | ||
| 119 | wheels = [ | ||
| 120 | { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105 }, | ||
| 121 | { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799 }, | ||
| 122 | { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504 }, | ||
| 123 | { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542 }, | ||
| 124 | { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244 }, | ||
| 125 | { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975 }, | ||
| 126 | { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082 }, | ||
| 127 | { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397 }, | ||
| 128 | { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244 }, | ||
| 129 | { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862 }, | ||
| 130 | { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578 }, | ||
| 131 | { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400 }, | ||
| 132 | { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824 }, | ||
| 133 | { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233 }, | ||
| 134 | { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075 }, | ||
| 135 | { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517 }, | ||
| 136 | { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893 }, | ||
| 137 | { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132 }, | ||
| 138 | { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086 }, | ||
| 139 | { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383 }, | ||
| 140 | { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186 }, | ||
| 141 | { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639 }, | ||
| 142 | { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552 }, | ||
| 143 | { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742 }, | ||
| 144 | ] | ||
| 145 | |||
| 146 | [[package]] | ||
| 147 | name = "daphne" | ||
| 148 | version = "4.2.1" | ||
| 149 | source = { registry = "https://pypi.org/simple" } | ||
| 150 | dependencies = [ | ||
| 151 | { name = "asgiref" }, | ||
| 152 | { name = "autobahn" }, | ||
| 153 | { name = "twisted", extra = ["tls"] }, | ||
| 154 | ] | ||
| 155 | sdist = { url = "https://files.pythonhosted.org/packages/cd/9d/322b605fdc03b963cf2d33943321c8f4405e8d82e698bf49d1eed1ca40c4/daphne-4.2.1.tar.gz", hash = "sha256:5f898e700a1fda7addf1541d7c328606415e96a7bd768405f0463c312fcb31b3", size = 45600 } | ||
| 156 | wheels = [ | ||
| 157 | { url = "https://files.pythonhosted.org/packages/01/34/6171ab34715ed210bcd6c2b38839cc792993cff4fe2493f50bc92b0086a0/daphne-4.2.1-py3-none-any.whl", hash = "sha256:881e96b387b95b35ad85acd855f229d7f5b79073d6649089c8a33f661885e055", size = 29015 }, | ||
| 158 | ] | ||
| 159 | |||
| 160 | [[package]] | ||
| 161 | name = "django" | ||
| 162 | version = "5.2.5" | ||
| 163 | source = { registry = "https://pypi.org/simple" } | ||
| 164 | dependencies = [ | ||
| 165 | { name = "asgiref" }, | ||
| 166 | { name = "sqlparse" }, | ||
| 167 | { name = "tzdata", marker = "sys_platform == 'win32'" }, | ||
| 168 | ] | ||
| 169 | sdist = { url = "https://files.pythonhosted.org/packages/62/9b/779f853c3d2d58b9e08346061ff3e331cdec3fe3f53aae509e256412a593/django-5.2.5.tar.gz", hash = "sha256:0745b25681b129a77aae3d4f6549b62d3913d74407831abaa0d9021a03954bae", size = 10859748 } | ||
| 170 | wheels = [ | ||
| 171 | { url = "https://files.pythonhosted.org/packages/9d/6e/98a1d23648e0085bb5825326af17612ecd8fc76be0ce96ea4dc35e17b926/django-5.2.5-py3-none-any.whl", hash = "sha256:2b2ada0ee8a5ff743a40e2b9820d1f8e24c11bac9ae6469cd548f0057ea6ddcd", size = 8302999 }, | ||
| 172 | ] | ||
| 173 | |||
| 174 | [[package]] | ||
| 175 | name = "hyperlink" | ||
| 176 | version = "21.0.0" | ||
| 177 | source = { registry = "https://pypi.org/simple" } | ||
| 178 | dependencies = [ | ||
| 179 | { name = "idna" }, | ||
| 180 | ] | ||
| 181 | sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743 } | ||
| 182 | wheels = [ | ||
| 183 | { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638 }, | ||
| 184 | ] | ||
| 185 | |||
| 186 | [[package]] | ||
| 187 | name = "idna" | ||
| 188 | version = "3.10" | ||
| 189 | source = { registry = "https://pypi.org/simple" } | ||
| 190 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } | ||
| 191 | wheels = [ | ||
| 192 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, | ||
| 193 | ] | ||
| 194 | |||
| 195 | [[package]] | ||
| 196 | name = "incremental" | ||
| 197 | version = "24.7.2" | ||
| 198 | source = { registry = "https://pypi.org/simple" } | ||
| 199 | dependencies = [ | ||
| 200 | { name = "setuptools" }, | ||
| 201 | ] | ||
| 202 | sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157 } | ||
| 203 | wheels = [ | ||
| 204 | { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516 }, | ||
| 205 | ] | ||
| 206 | |||
| 207 | [[package]] | ||
| 208 | name = "msgpack" | ||
| 209 | version = "1.1.1" | ||
| 210 | source = { registry = "https://pypi.org/simple" } | ||
| 211 | sdist = { url = "https://files.pythonhosted.org/packages/45/b1/ea4f68038a18c77c9467400d166d74c4ffa536f34761f7983a104357e614/msgpack-1.1.1.tar.gz", hash = "sha256:77b79ce34a2bdab2594f490c8e80dd62a02d650b91a75159a63ec413b8d104cd", size = 173555 } | ||
| 212 | wheels = [ | ||
| 213 | { url = "https://files.pythonhosted.org/packages/a1/38/561f01cf3577430b59b340b51329803d3a5bf6a45864a55f4ef308ac11e3/msgpack-1.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3765afa6bd4832fc11c3749be4ba4b69a0e8d7b728f78e68120a157a4c5d41f0", size = 81677 }, | ||
| 214 | { url = "https://files.pythonhosted.org/packages/09/48/54a89579ea36b6ae0ee001cba8c61f776451fad3c9306cd80f5b5c55be87/msgpack-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8ddb2bcfd1a8b9e431c8d6f4f7db0773084e107730ecf3472f1dfe9ad583f3d9", size = 78603 }, | ||
| 215 | { url = "https://files.pythonhosted.org/packages/a0/60/daba2699b308e95ae792cdc2ef092a38eb5ee422f9d2fbd4101526d8a210/msgpack-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:196a736f0526a03653d829d7d4c5500a97eea3648aebfd4b6743875f28aa2af8", size = 420504 }, | ||
| 216 | { url = "https://files.pythonhosted.org/packages/20/22/2ebae7ae43cd8f2debc35c631172ddf14e2a87ffcc04cf43ff9df9fff0d3/msgpack-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d592d06e3cc2f537ceeeb23d38799c6ad83255289bb84c2e5792e5a8dea268a", size = 423749 }, | ||
| 217 | { url = "https://files.pythonhosted.org/packages/40/1b/54c08dd5452427e1179a40b4b607e37e2664bca1c790c60c442c8e972e47/msgpack-1.1.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4df2311b0ce24f06ba253fda361f938dfecd7b961576f9be3f3fbd60e87130ac", size = 404458 }, | ||
| 218 | { url = "https://files.pythonhosted.org/packages/2e/60/6bb17e9ffb080616a51f09928fdd5cac1353c9becc6c4a8abd4e57269a16/msgpack-1.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e4141c5a32b5e37905b5940aacbc59739f036930367d7acce7a64e4dec1f5e0b", size = 405976 }, | ||
| 219 | { url = "https://files.pythonhosted.org/packages/ee/97/88983e266572e8707c1f4b99c8fd04f9eb97b43f2db40e3172d87d8642db/msgpack-1.1.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b1ce7f41670c5a69e1389420436f41385b1aa2504c3b0c30620764b15dded2e7", size = 408607 }, | ||
| 220 | { url = "https://files.pythonhosted.org/packages/bc/66/36c78af2efaffcc15a5a61ae0df53a1d025f2680122e2a9eb8442fed3ae4/msgpack-1.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4147151acabb9caed4e474c3344181e91ff7a388b888f1e19ea04f7e73dc7ad5", size = 424172 }, | ||
| 221 | { url = "https://files.pythonhosted.org/packages/8c/87/a75eb622b555708fe0427fab96056d39d4c9892b0c784b3a721088c7ee37/msgpack-1.1.1-cp313-cp313-win32.whl", hash = "sha256:500e85823a27d6d9bba1d057c871b4210c1dd6fb01fbb764e37e4e8847376323", size = 65347 }, | ||
| 222 | { url = "https://files.pythonhosted.org/packages/ca/91/7dc28d5e2a11a5ad804cf2b7f7a5fcb1eb5a4966d66a5d2b41aee6376543/msgpack-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:6d489fba546295983abd142812bda76b57e33d0b9f5d5b71c09a583285506f69", size = 72341 }, | ||
| 223 | ] | ||
| 224 | |||
| 225 | [[package]] | ||
| 226 | name = "pyasn1" | ||
| 227 | version = "0.6.1" | ||
| 228 | source = { registry = "https://pypi.org/simple" } | ||
| 229 | sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } | ||
| 230 | wheels = [ | ||
| 231 | { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, | ||
| 232 | ] | ||
| 233 | |||
| 234 | [[package]] | ||
| 235 | name = "pyasn1-modules" | ||
| 236 | version = "0.4.2" | ||
| 237 | source = { registry = "https://pypi.org/simple" } | ||
| 238 | dependencies = [ | ||
| 239 | { name = "pyasn1" }, | ||
| 240 | ] | ||
| 241 | sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } | ||
| 242 | wheels = [ | ||
| 243 | { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, | ||
| 244 | ] | ||
| 245 | |||
| 246 | [[package]] | ||
| 247 | name = "pycparser" | ||
| 248 | version = "2.22" | ||
| 249 | source = { registry = "https://pypi.org/simple" } | ||
| 250 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } | ||
| 251 | wheels = [ | ||
| 252 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, | ||
| 253 | ] | ||
| 254 | |||
| 255 | [[package]] | ||
| 256 | name = "pyopenssl" | ||
| 257 | version = "25.1.0" | ||
| 258 | source = { registry = "https://pypi.org/simple" } | ||
| 259 | dependencies = [ | ||
| 260 | { name = "cryptography" }, | ||
| 261 | ] | ||
| 262 | sdist = { url = "https://files.pythonhosted.org/packages/04/8c/cd89ad05804f8e3c17dea8f178c3f40eeab5694c30e0c9f5bcd49f576fc3/pyopenssl-25.1.0.tar.gz", hash = "sha256:8d031884482e0c67ee92bf9a4d8cceb08d92aba7136432ffb0703c5280fc205b", size = 179937 } | ||
| 263 | wheels = [ | ||
| 264 | { url = "https://files.pythonhosted.org/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771 }, | ||
| 265 | ] | ||
| 266 | |||
| 267 | [[package]] | ||
| 268 | name = "redis" | ||
| 269 | version = "6.4.0" | ||
| 270 | source = { registry = "https://pypi.org/simple" } | ||
| 271 | sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399 } | ||
| 272 | wheels = [ | ||
| 273 | { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847 }, | ||
| 274 | ] | ||
| 275 | |||
| 276 | [[package]] | ||
| 277 | name = "service-identity" | ||
| 278 | version = "24.2.0" | ||
| 279 | source = { registry = "https://pypi.org/simple" } | ||
| 280 | dependencies = [ | ||
| 281 | { name = "attrs" }, | ||
| 282 | { name = "cryptography" }, | ||
| 283 | { name = "pyasn1" }, | ||
| 284 | { name = "pyasn1-modules" }, | ||
| 285 | ] | ||
| 286 | sdist = { url = "https://files.pythonhosted.org/packages/07/a5/dfc752b979067947261dbbf2543470c58efe735c3c1301dd870ef27830ee/service_identity-24.2.0.tar.gz", hash = "sha256:b8683ba13f0d39c6cd5d625d2c5f65421d6d707b013b375c355751557cbe8e09", size = 39245 } | ||
| 287 | wheels = [ | ||
| 288 | { url = "https://files.pythonhosted.org/packages/08/2c/ca6dd598b384bc1ce581e24aaae0f2bed4ccac57749d5c3befbb5e742081/service_identity-24.2.0-py3-none-any.whl", hash = "sha256:6b047fbd8a84fd0bb0d55ebce4031e400562b9196e1e0d3e0fe2b8a59f6d4a85", size = 11364 }, | ||
| 289 | ] | ||
| 290 | |||
| 291 | [[package]] | ||
| 292 | name = "setuptools" | ||
| 293 | version = "80.9.0" | ||
| 294 | source = { registry = "https://pypi.org/simple" } | ||
| 295 | sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958 } | ||
| 296 | wheels = [ | ||
| 297 | { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486 }, | ||
| 298 | ] | ||
| 299 | |||
| 300 | [[package]] | ||
| 301 | name = "sqlparse" | ||
| 302 | version = "0.5.3" | ||
| 303 | source = { registry = "https://pypi.org/simple" } | ||
| 304 | sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } | ||
| 305 | wheels = [ | ||
| 306 | { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, | ||
| 307 | ] | ||
| 308 | |||
| 309 | [[package]] | ||
| 310 | name = "twisted" | ||
| 311 | version = "25.5.0" | ||
| 312 | source = { registry = "https://pypi.org/simple" } | ||
| 313 | dependencies = [ | ||
| 314 | { name = "attrs" }, | ||
| 315 | { name = "automat" }, | ||
| 316 | { name = "constantly" }, | ||
| 317 | { name = "hyperlink" }, | ||
| 318 | { name = "incremental" }, | ||
| 319 | { name = "typing-extensions" }, | ||
| 320 | { name = "zope-interface" }, | ||
| 321 | ] | ||
| 322 | sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725 } | ||
| 323 | wheels = [ | ||
| 324 | { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767 }, | ||
| 325 | ] | ||
| 326 | |||
| 327 | [package.optional-dependencies] | ||
| 328 | tls = [ | ||
| 329 | { name = "idna" }, | ||
| 330 | { name = "pyopenssl" }, | ||
| 331 | { name = "service-identity" }, | ||
| 332 | ] | ||
| 333 | |||
| 334 | [[package]] | ||
| 335 | name = "txaio" | ||
| 336 | version = "25.6.1" | ||
| 337 | source = { registry = "https://pypi.org/simple" } | ||
| 338 | sdist = { url = "https://files.pythonhosted.org/packages/10/21/f1d3305ae1d3ca05aa71d509f02f4db11c1357001f7e31f9713e610efc5b/txaio-25.6.1.tar.gz", hash = "sha256:d8c03dca823515c9bca920df33504923ae54f2dabf476cc5a9ed5cc1691ed687", size = 58709 } | ||
| 339 | wheels = [ | ||
| 340 | { url = "https://files.pythonhosted.org/packages/52/56/aff5af8caa210321d0c206eb19897a4e0b29b4e24c4e24226325950efe0b/txaio-25.6.1-py2.py3-none-any.whl", hash = "sha256:f461b917a14d46077fb1668d0bea4998695d93c9c569cd05fd7f193abdd22414", size = 31250 }, | ||
| 341 | ] | ||
| 342 | |||
| 343 | [[package]] | ||
| 344 | name = "typing-extensions" | ||
| 345 | version = "4.15.0" | ||
| 346 | source = { registry = "https://pypi.org/simple" } | ||
| 347 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } | ||
| 348 | wheels = [ | ||
| 349 | { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, | ||
| 350 | ] | ||
| 351 | |||
| 352 | [[package]] | ||
| 353 | name = "tzdata" | ||
| 354 | version = "2025.2" | ||
| 355 | source = { registry = "https://pypi.org/simple" } | ||
| 356 | sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 } | ||
| 357 | wheels = [ | ||
| 358 | { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 }, | ||
| 359 | ] | ||
| 360 | |||
| 361 | [[package]] | ||
| 362 | name = "websocket-chat" | ||
| 363 | version = "0.1.0" | ||
| 364 | source = { virtual = "." } | ||
| 365 | dependencies = [ | ||
| 366 | { name = "channels", extra = ["daphne"] }, | ||
| 367 | { name = "channels-redis" }, | ||
| 368 | { name = "django" }, | ||
| 369 | ] | ||
| 370 | |||
| 371 | [package.metadata] | ||
| 372 | requires-dist = [ | ||
| 373 | { name = "channels", extras = ["daphne"], specifier = ">=4.3.1" }, | ||
| 374 | { name = "channels-redis", specifier = ">=4.3.0" }, | ||
| 375 | { name = "django", specifier = ">=5.2.5" }, | ||
| 376 | ] | ||
| 377 | |||
| 378 | [[package]] | ||
| 379 | name = "zope-interface" | ||
| 380 | version = "7.2" | ||
| 381 | source = { registry = "https://pypi.org/simple" } | ||
| 382 | dependencies = [ | ||
| 383 | { name = "setuptools" }, | ||
| 384 | ] | ||
| 385 | sdist = { url = "https://files.pythonhosted.org/packages/30/93/9210e7606be57a2dfc6277ac97dcc864fd8d39f142ca194fdc186d596fda/zope.interface-7.2.tar.gz", hash = "sha256:8b49f1a3d1ee4cdaf5b32d2e738362c7f5e40ac8b46dd7d1a65e82a4872728fe", size = 252960 } | ||
| 386 | wheels = [ | ||
| 387 | { url = "https://files.pythonhosted.org/packages/c6/3b/e309d731712c1a1866d61b5356a069dd44e5b01e394b6cb49848fa2efbff/zope.interface-7.2-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:3e0350b51e88658d5ad126c6a57502b19d5f559f6cb0a628e3dc90442b53dd98", size = 208961 }, | ||
| 388 | { url = "https://files.pythonhosted.org/packages/49/65/78e7cebca6be07c8fc4032bfbb123e500d60efdf7b86727bb8a071992108/zope.interface-7.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15398c000c094b8855d7d74f4fdc9e73aa02d4d0d5c775acdef98cdb1119768d", size = 209356 }, | ||
| 389 | { url = "https://files.pythonhosted.org/packages/11/b1/627384b745310d082d29e3695db5f5a9188186676912c14b61a78bbc6afe/zope.interface-7.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:802176a9f99bd8cc276dcd3b8512808716492f6f557c11196d42e26c01a69a4c", size = 264196 }, | ||
| 390 | { url = "https://files.pythonhosted.org/packages/b8/f6/54548df6dc73e30ac6c8a7ff1da73ac9007ba38f866397091d5a82237bd3/zope.interface-7.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb23f58a446a7f09db85eda09521a498e109f137b85fb278edb2e34841055398", size = 259237 }, | ||
| 391 | { url = "https://files.pythonhosted.org/packages/b6/66/ac05b741c2129fdf668b85631d2268421c5cd1a9ff99be1674371139d665/zope.interface-7.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a71a5b541078d0ebe373a81a3b7e71432c61d12e660f1d67896ca62d9628045b", size = 264696 }, | ||
| 392 | { url = "https://files.pythonhosted.org/packages/0a/2f/1bccc6f4cc882662162a1158cda1a7f616add2ffe322b28c99cb031b4ffc/zope.interface-7.2-cp313-cp313-win_amd64.whl", hash = "sha256:4893395d5dd2ba655c38ceb13014fd65667740f09fa5bb01caa1e6284e48c0cd", size = 212472 }, | ||
| 393 | ] | ||
