diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 002455cd2..9555f3bbc 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -73,6 +73,7 @@ jobs: with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} + source-root: . # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. diff --git a/poetry.lock b/poetry.lock index 5fba1b224..7261e4ca4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "asgiref" @@ -6,7 +6,6 @@ version = "3.9.1" description = "ASGI specs, helper code, and adapters" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "asgiref-3.9.1-py3-none-any.whl", hash = "sha256:f3bba7092a48005b5f5bacd747d36ee4a5a61f4a269a6df590b43144355ebd2c"}, {file = "asgiref-3.9.1.tar.gz", hash = "sha256:a5ab6582236218e5ef1648f242fd9f10626cfd4de8dc377db215d5d5098e3142"}, @@ -24,8 +23,6 @@ version = "5.0.1" description = "Timeout context manager for asyncio programs" optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "python_full_version < \"3.11.3\"" files = [ {file = "async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c"}, {file = "async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3"}, @@ -37,7 +34,6 @@ version = "6.2.0" description = "An easy safelist-based HTML-sanitizing tool." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e"}, {file = "bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f"}, @@ -55,7 +51,6 @@ version = "5.5.1" description = "Extensible memoizing collections and decorators" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "cachetools-5.5.1-py3-none-any.whl", hash = "sha256:b76651fdc3b24ead3c648bbdeeb940c1b04d365b38b4af66788f9ec4a81d42bb"}, {file = "cachetools-5.5.1.tar.gz", hash = "sha256:70f238fbba50383ef62e55c6aff6d9673175fe59f7c6782c7a0b9e38f4a9df95"}, @@ -67,7 +62,6 @@ version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -79,8 +73,6 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -160,7 +152,6 @@ version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, @@ -172,7 +163,6 @@ version = "4.3.1" description = "Brings async, event-driven capabilities to Django." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "channels-4.3.1-py3-none-any.whl", hash = "sha256:b091d4b26f91d807de3e84aead7ba785314f27eaf5bac31dd51b1c956b883859"}, {file = "channels-4.3.1.tar.gz", hash = "sha256:97413ffd674542db08e16a9ef09cd86ec0113e5f8125fbd33cf0854adcf27cdb"}, @@ -192,7 +182,6 @@ version = "4.3.0" description = "Redis-backed ASGI channel layer implementation" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "channels_redis-4.3.0-py3-none-any.whl", hash = "sha256:48f3e902ae2d5fef7080215524f3b4a1d3cea4e304150678f867a1a822c0d9f5"}, {file = "channels_redis-4.3.0.tar.gz", hash = "sha256:740ee7b54f0e28cf2264a940a24453d3f00526a96931f911fcb69228ef245dd2"}, @@ -214,7 +203,6 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -316,7 +304,6 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -331,7 +318,6 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -343,7 +329,6 @@ version = "44.0.2" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = "!=3.9.0,!=3.9.1,>=3.7" -groups = ["main"] files = [ {file = "cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7"}, {file = "cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1"}, @@ -386,10 +371,10 @@ files = [ cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} [package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0) ; python_version >= \"3.8\""] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=3.0.0)"] docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"] -nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2) ; python_version >= \"3.8\""] -pep8test = ["check-sdist ; python_version >= \"3.8\"", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] +nox = ["nox (>=2024.4.15)", "nox[uv] (>=2024.3.2)"] +pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.4)", "ruff (>=0.3.6)"] sdist = ["build (>=1.0.0)"] ssh = ["bcrypt (>=3.1.5)"] test = ["certifi (>=2024)", "cryptography-vectors (==44.0.2)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"] @@ -401,7 +386,6 @@ version = "1.15.3" description = "CSS unobfuscator and beautifier." optional = false python-versions = "*" -groups = ["main", "dev"] files = [ {file = "cssbeautifier-1.15.3-py3-none-any.whl", hash = "sha256:0dcaf5ce197743a79b3a160b84ea58fcbd9e3e767c96df1171e428125b16d410"}, {file = "cssbeautifier-1.15.3.tar.gz", hash = "sha256:406b04d09e7d62c0be084fbfa2cba5126fe37359ea0d8d9f7b963a6354fc8303"}, @@ -412,13 +396,23 @@ editorconfig = ">=0.12.2" jsbeautifier = "*" six = ">=1.13.0" +[[package]] +name = "defusedxml" +version = "0.7.1" +description = "XML bomb protection for Python stdlib modules" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, + {file = "defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69"}, +] + [[package]] name = "distlib" version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" -groups = ["dev"] files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, @@ -430,7 +424,6 @@ version = "5.1.15" description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design." optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "django-5.1.15-py3-none-any.whl", hash = "sha256:117871e58d6eda37f09870b7d73a3d66567b03aecd515b386b1751177c413432"}, {file = "django-5.1.15.tar.gz", hash = "sha256:46a356b5ff867bece73fc6365e081f21c569973403ee7e9b9a0316f27d0eb947"}, @@ -451,7 +444,6 @@ version = "65.4.1" description = "Integrated set of Django applications addressing authentication, registration, account management as well as 3rd party (social) account authentication." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "django_allauth-65.4.1.tar.gz", hash = "sha256:60b32aef7dbbcc213319aa4fd8f570e985266ea1162ae6ef7a26a24efca85c8c"}, ] @@ -473,7 +465,6 @@ version = "1.18.0" description = "Automatically reload your browser in development." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "django_browser_reload-1.18.0-py3-none-any.whl", hash = "sha256:ed4cc2fb83c3bf6c30b54107a1a6736c0b896e62e4eba666d81005b9f2ecf6f8"}, {file = "django_browser_reload-1.18.0.tar.gz", hash = "sha256:c5f0b134723cbf2a0dc9ae1ee1d38e42db28fe23c74cdee613ba3ef286d04735"}, @@ -489,7 +480,6 @@ version = "0.11.2" description = "A package that allows you to utilize 12factor inspired environment variables to configure your Django application." optional = false python-versions = ">=3.6,<4" -groups = ["main"] files = [ {file = "django-environ-0.11.2.tar.gz", hash = "sha256:f32a87aa0899894c27d4e1776fa6b477e8164ed7f6b3e410a62a6d72caaf64be"}, {file = "django_environ-0.11.2-py2.py3-none-any.whl", hash = "sha256:0ff95ab4344bfeff693836aa978e6840abef2e2f1145adff7735892711590c05"}, @@ -506,7 +496,6 @@ version = "4.0.7" description = "A comprehensive Markdown editor built for Django." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django-markdownx-4.0.7.tar.gz", hash = "sha256:38aa331c2ca0bee218b77f462361b5393e4727962bc6021939c09048363cb6ea"}, {file = "django_markdownx-4.0.7-py2.py3-none-any.whl", hash = "sha256:c1975ae3053481d4c111abd38997a5b5bb89235a1e3215f995d835942925fe7b"}, @@ -523,7 +512,6 @@ version = "0.2.0" description = "Modified Django FileResponse that adds Content-Range headers." optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django-ranged-response-0.2.0.tar.gz", hash = "sha256:f71fff352a37316b9bead717fc76e4ddd6c9b99c4680cdf4783b9755af1cf985"}, ] @@ -537,7 +525,6 @@ version = "5.4.0" description = "Full featured redis cache backend for Django." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "django-redis-5.4.0.tar.gz", hash = "sha256:6a02abaa34b0fea8bf9b707d2c363ab6adc7409950b2db93602e6cb292818c42"}, {file = "django_redis-5.4.0-py3-none-any.whl", hash = "sha256:ebc88df7da810732e2af9987f7f426c96204bf89319df4c6da6ca9a2942edd5b"}, @@ -556,7 +543,6 @@ version = "0.5.20" description = "A very simple, yet powerful, Django captcha application" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "django-simple-captcha-0.5.20.tar.gz", hash = "sha256:20273009a7beb44297e9f6c7a6bd21ada3d2fa93c314d2f6bf5e394ceeb6a297"}, {file = "django_simple_captcha-0.5.20-py2.py3-none-any.whl", hash = "sha256:3359cb033c489eae6544a80ad92517db3d35b3b328b3b427393399c3d7f55275"}, @@ -576,7 +562,6 @@ version = "1.14.4" description = "Support for many storage backends in Django" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "django-storages-1.14.4.tar.gz", hash = "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f"}, {file = "django_storages-1.14.4-py3-none-any.whl", hash = "sha256:d61930acb4a25e3aebebc6addaf946a3b1df31c803a6bf1af2f31c9047febaa3"}, @@ -600,7 +585,6 @@ version = "1.36.4" description = "HTML Template Linter and Formatter" optional = false python-versions = ">=3.9" -groups = ["main", "dev"] files = [ {file = "djlint-1.36.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2dfb60883ceb92465201bfd392291a7597c6752baede6fbb6f1980cac8d6c5c"}, {file = "djlint-1.36.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4bc6a1320c0030244b530ac200642f883d3daa451a115920ef3d56d08b644292"}, @@ -645,7 +629,6 @@ version = "0.17.0" description = "EditorConfig File Locator and Interpreter for Python" optional = false python-versions = "*" -groups = ["main", "dev"] files = [ {file = "EditorConfig-0.17.0-py3-none-any.whl", hash = "sha256:fe491719c5f65959ec00b167d07740e7ffec9a3f362038c72b289330b9991dfc"}, {file = "editorconfig-0.17.0.tar.gz", hash = "sha256:8739052279699840065d3a9f5c125d7d5a98daeefe53b0e5274261d77cb49aa2"}, @@ -657,7 +640,6 @@ version = "3.17.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, @@ -666,7 +648,7 @@ files = [ [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "google-api-core" @@ -674,7 +656,6 @@ version = "2.24.1" description = "Google API client core library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "google_api_core-2.24.1-py3-none-any.whl", hash = "sha256:bc78d608f5a5bf853b80bd70a795f703294de656c096c0968320830a4bc280f1"}, {file = "google_api_core-2.24.1.tar.gz", hash = "sha256:f8b36f5456ab0dd99a1b693a40a31d1e7757beea380ad1b38faaf8941eae9d8a"}, @@ -684,15 +665,15 @@ files = [ google-auth = ">=2.14.1,<3.0.dev0" googleapis-common-protos = ">=1.56.2,<2.0.dev0" proto-plus = [ - {version = ">=1.25.0,<2.0.0.dev0", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0.dev0", markers = "python_version < \"3.13\""}, + {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev", markers = "python_version < \"3.13\""}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0.dev0" requests = ">=2.18.0,<3.0.0.dev0" [package.extras] async-rest = ["google-auth[aiohttp] (>=2.35.0,<3.0.dev0)"] -grpc = ["grpcio (>=1.33.2,<2.0.dev0)", "grpcio (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\"", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0) ; python_version >= \"3.11\""] +grpc = ["grpcio (>=1.33.2,<2.0dev)", "grpcio (>=1.49.1,<2.0dev)", "grpcio-status (>=1.33.2,<2.0.dev0)", "grpcio-status (>=1.49.1,<2.0.dev0)"] grpcgcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] grpcio-gcp = ["grpcio-gcp (>=0.2.2,<1.0.dev0)"] @@ -702,7 +683,6 @@ version = "2.161.0" description = "Google API Client Library for Python" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "google_api_python_client-2.161.0-py2.py3-none-any.whl", hash = "sha256:9476a5a4f200bae368140453df40f9cda36be53fa7d0e9a9aac4cdb859a26448"}, {file = "google_api_python_client-2.161.0.tar.gz", hash = "sha256:324c0cce73e9ea0a0d2afd5937e01b7c2d6a4d7e2579cdb6c384f9699d6c9f37"}, @@ -721,7 +701,6 @@ version = "2.38.0" description = "Google Authentication Library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "google_auth-2.38.0-py2.py3-none-any.whl", hash = "sha256:e7dae6694313f434a2727bf2906f27ad259bae090d7aa896590d86feec3d9d4a"}, {file = "google_auth-2.38.0.tar.gz", hash = "sha256:8285113607d3b80a3f1543b75962447ba8a09fe85783432a784fdeef6ac094c4"}, @@ -746,7 +725,6 @@ version = "0.2.0" description = "Google Authentication Library: httplib2 transport" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "google-auth-httplib2-0.2.0.tar.gz", hash = "sha256:38aa7badf48f974f1eb9861794e9c0cb2a0511a4ec0679b1f886d108f5640e05"}, {file = "google_auth_httplib2-0.2.0-py2.py3-none-any.whl", hash = "sha256:b65a0a2123300dd71281a7bf6e64d65a0759287df52729bdd1ae2e47dc311a3d"}, @@ -762,7 +740,6 @@ version = "1.2.1" description = "Google Authentication Library" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "google_auth_oauthlib-1.2.1-py2.py3-none-any.whl", hash = "sha256:2d58a27262d55aa1b87678c3ba7142a080098cbc2024f903c62355deb235d91f"}, {file = "google_auth_oauthlib-1.2.1.tar.gz", hash = "sha256:afd0cad092a2eaa53cd8e8298557d6de1034c6cb4a740500b5357b648af97263"}, @@ -781,7 +758,6 @@ version = "1.67.0" description = "Common protobufs used in Google APIs" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "googleapis_common_protos-1.67.0-py2.py3-none-any.whl", hash = "sha256:579de760800d13616f51cf8be00c876f00a9f146d3e6510e19d1f4111758b741"}, {file = "googleapis_common_protos-1.67.0.tar.gz", hash = "sha256:21398025365f138be356d5923e9168737d94d46a72aefee4a6110a1f23463c86"}, @@ -799,7 +775,6 @@ version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, @@ -811,7 +786,6 @@ version = "0.22.0" description = "A comprehensive HTTP client library." optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] files = [ {file = "httplib2-0.22.0-py3-none-any.whl", hash = "sha256:14ae0a53c1ba8f3d37e9e27cf37eabb0fb9980f435ba405d546948b009dd64dc"}, {file = "httplib2-0.22.0.tar.gz", hash = "sha256:d7a10bc5ef5ab08322488bde8c726eeee5c8618723fdb399597ec58f3d82df81"}, @@ -826,7 +800,6 @@ version = "5.0.13" description = "iCalendar parser/generator" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "icalendar-5.0.13-py3-none-any.whl", hash = "sha256:5ded5415e2e1edef5ab230024a75878a7a81d518a3b1ae4f34bf20b173c84dc2"}, {file = "icalendar-5.0.13.tar.gz", hash = "sha256:92799fde8cce0b61daa8383593836d1e19136e504fa1671f471f98be9b029706"}, @@ -842,7 +815,6 @@ version = "2.6.7" description = "File identification library for Python" optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "identify-2.6.7-py2.py3-none-any.whl", hash = "sha256:155931cb617a401807b09ecec6635d6c692d180090a1cedca8ef7d58ba5b6aa0"}, {file = "identify-2.6.7.tar.gz", hash = "sha256:3fa266b42eba321ee0b2bb0936a6a6b9e36a1351cbb69055b3082f4193035684"}, @@ -857,7 +829,6 @@ version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -872,7 +843,6 @@ version = "1.15.3" description = "JavaScript unobfuscator and beautifier." optional = false python-versions = "*" -groups = ["main", "dev"] files = [ {file = "jsbeautifier-1.15.3-py3-none-any.whl", hash = "sha256:b207a15ab7529eee4a35ae7790e9ec4e32a2b5026d51e2d0386c3a65e6ecfc91"}, {file = "jsbeautifier-1.15.3.tar.gz", hash = "sha256:5f1baf3d4ca6a615bb5417ee861b34b77609eeb12875555f8bbfabd9bf2f3457"}, @@ -888,7 +858,6 @@ version = "0.10.0" description = "A Python implementation of the JSON5 data format." optional = false python-versions = ">=3.8.0" -groups = ["main", "dev"] files = [ {file = "json5-0.10.0-py3-none-any.whl", hash = "sha256:19b23410220a7271e8377f81ba8aacba2fdd56947fbb137ee5977cbe1f5e8dfa"}, {file = "json5-0.10.0.tar.gz", hash = "sha256:e66941c8f0a02026943c52c2eb34ebeb2a6f819a0be05920a6f5243cd30fd559"}, @@ -903,7 +872,6 @@ version = "3.7" description = "Python implementation of John Gruber's Markdown." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "Markdown-3.7-py3-none-any.whl", hash = "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803"}, {file = "markdown-3.7.tar.gz", hash = "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2"}, @@ -919,7 +887,6 @@ version = "1.1.1" description = "MessagePack serializer" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "msgpack-1.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:353b6fc0c36fde68b661a12949d7d49f8f51ff5fa019c1e47c87c4ff34b080ed"}, {file = "msgpack-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:79c408fcf76a958491b4e3b103d1c417044544b68e96d06432a189b43d1215c8"}, @@ -988,7 +955,6 @@ version = "2.2.7" description = "Python interface to MySQL" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "mysqlclient-2.2.7-cp310-cp310-win_amd64.whl", hash = "sha256:2e3c11f7625029d7276ca506f8960a7fd3c5a0a0122c9e7404e6a8fe961b3d22"}, {file = "mysqlclient-2.2.7-cp311-cp311-win_amd64.whl", hash = "sha256:a22d99d26baf4af68ebef430e3131bb5a9b722b79a9fcfac6d9bbf8a88800687"}, @@ -1006,7 +972,6 @@ version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, @@ -1018,7 +983,6 @@ version = "4.1.3" description = "OAuth 2.0 client library" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "oauth2client-4.1.3-py2.py3-none-any.whl", hash = "sha256:b8a81cc5d60e2d364f0b1b98f958dbd472887acaf1a5b05e21c28c31a2d6d3ac"}, {file = "oauth2client-4.1.3.tar.gz", hash = "sha256:d486741e451287f69568a4d26d70d9acd73a2bbfa275746c535b4209891cccc6"}, @@ -1037,7 +1001,6 @@ version = "3.2.2" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca"}, {file = "oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918"}, @@ -1054,7 +1017,6 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, @@ -1066,7 +1028,6 @@ version = "12.1.1" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0"}, {file = "pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713"}, @@ -1175,7 +1136,6 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -1192,7 +1152,6 @@ version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, @@ -1211,14 +1170,13 @@ version = "1.26.0" description = "Beautiful, Pythonic protocol buffers" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "proto_plus-1.26.0-py3-none-any.whl", hash = "sha256:bf2dfaa3da281fc3187d12d224c707cb57214fb2c22ba854eb0c105a3fb2d4d7"}, {file = "proto_plus-1.26.0.tar.gz", hash = "sha256:6e93d5f5ca267b54300880fff156b6a3386b3fa3f43b1da62e680fc0c586ef22"}, ] [package.dependencies] -protobuf = ">=3.19.0,<6.0.0.dev0" +protobuf = ">=3.19.0,<6.0.0dev" [package.extras] testing = ["google-api-core (>=1.31.5)"] @@ -1229,7 +1187,6 @@ version = "5.29.3" description = "" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "protobuf-5.29.3-cp310-abi3-win32.whl", hash = "sha256:3ea51771449e1035f26069c4c7fd51fba990d07bc55ba80701c78f886bf9c888"}, {file = "protobuf-5.29.3-cp310-abi3-win_amd64.whl", hash = "sha256:a4fa6f80816a9a0678429e84973f2f98cbc218cca434abe8db2ad0bffc98503a"}, @@ -1250,7 +1207,6 @@ version = "7.1.3" description = "Cross-platform lib for process and system monitoring." optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc"}, {file = "psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0"}, @@ -1274,8 +1230,8 @@ files = [ ] [package.extras] -dev = ["abi3audit", "black", "check-manifest", "colorama ; os_name == \"nt\"", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline ; os_name == \"nt\"", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] -test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32 ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "setuptools", "wheel ; os_name == \"nt\" and platform_python_implementation != \"PyPy\"", "wmi ; os_name == \"nt\" and platform_python_implementation != \"PyPy\""] +dev = ["abi3audit", "black", "check-manifest", "colorama", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pyreadline", "pytest", "pytest-cov", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "validate-pyproject[all]", "virtualenv", "vulture", "wheel", "wheel", "wmi"] +test = ["pytest", "pytest-instafail", "pytest-subtests", "pytest-xdist", "pywin32", "setuptools", "wheel", "wmi"] [[package]] name = "pyasn1" @@ -1283,7 +1239,6 @@ version = "0.6.1" description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629"}, {file = "pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034"}, @@ -1295,7 +1250,6 @@ version = "0.4.1" description = "A collection of ASN.1-based protocols modules" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "pyasn1_modules-0.4.1-py3-none-any.whl", hash = "sha256:49bfa96b45a292b711e986f222502c1c9a5e1f4e568fc30e2574a6c7d07838fd"}, {file = "pyasn1_modules-0.4.1.tar.gz", hash = "sha256:c28e2dbf9c06ad61c71a075c7e0f9fd0f1b0bb2d2ad4377f240d33ac2ab60a7c"}, @@ -1310,8 +1264,6 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" -groups = ["main"] -markers = "platform_python_implementation != \"PyPy\"" files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, @@ -1323,7 +1275,6 @@ version = "25.0.0" description = "Python wrapper module around the OpenSSL library" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "pyOpenSSL-25.0.0-py3-none-any.whl", hash = "sha256:424c247065e46e76a37411b9ab1782541c23bb658bf003772c3405fbaa128e90"}, {file = "pyopenssl-25.0.0.tar.gz", hash = "sha256:cd2cef799efa3936bb08e8ccb9433a575722b9dd986023f1cabc4ae64e9dac16"}, @@ -1343,7 +1294,6 @@ version = "3.2.1" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "pyparsing-3.2.1-py3-none-any.whl", hash = "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1"}, {file = "pyparsing-3.2.1.tar.gz", hash = "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"}, @@ -1358,7 +1308,6 @@ version = "1.4.1" description = "SVG avatar library for Python" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "python_avatars-1.4.1-py3-none-any.whl", hash = "sha256:ef97b1f8ac23583367705f876fbbac1cc118435982532d882ccc788e95663722"}, {file = "python_avatars-1.4.1.tar.gz", hash = "sha256:133dc0e1dfd778f0287aa6b6697da2677aeb3ce985ebf908205068e963165b0e"}, @@ -1370,7 +1319,6 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1385,7 +1333,6 @@ version = "2025.1" description = "World timezone definitions, modern and historical" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57"}, {file = "pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"}, @@ -1397,7 +1344,6 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, @@ -1460,7 +1406,6 @@ version = "6.4.0" description = "Python client for Redis database and key-value store" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f"}, {file = "redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010"}, @@ -1480,7 +1425,6 @@ version = "2024.11.6" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ff590880083d60acc0433f9c3f713c51f7ac6ebb9adf889c79a261ecf541aa91"}, {file = "regex-2024.11.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:658f90550f38270639e83ce492f27d2c8d2cd63805c65a13a14d36ca126753f0"}, @@ -1584,7 +1528,6 @@ version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -1606,7 +1549,6 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" -groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -1625,7 +1567,6 @@ version = "4.9" description = "Pure-Python RSA implementation" optional = false python-versions = ">=3.6,<4" -groups = ["main"] files = [ {file = "rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7"}, {file = "rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21"}, @@ -1640,7 +1581,6 @@ version = "2.25.1" description = "Python client for Sentry (https://sentry.io)" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "sentry_sdk-2.25.1-py2.py3-none-any.whl", hash = "sha256:60b016d0772789454dc55a284a6a44212044d4a16d9f8448725effee97aaf7f6"}, {file = "sentry_sdk-2.25.1.tar.gz", hash = "sha256:f9041b7054a7cf12d41eadabe6458ce7c6d6eea7a97cfe1b760b6692e9562cf0"}, @@ -1697,7 +1637,6 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main", "dev"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, @@ -1709,7 +1648,6 @@ version = "0.5.3" description = "A non-validating SQL parser." optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca"}, {file = "sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272"}, @@ -1725,7 +1663,6 @@ version = "11.5.0" description = "Python bindings for the Stripe API" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "stripe-11.5.0-py2.py3-none-any.whl", hash = "sha256:3b2cd47ed3002328249bff5cacaee38d5e756c3899ab425d3bd07acdaf32534a"}, {file = "stripe-11.5.0.tar.gz", hash = "sha256:bc3e0358ffc23d5ecfa8aafec1fa4f048ee8107c3237bcb00003e68c8c96fa02"}, @@ -1741,8 +1678,6 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] -markers = "python_version == \"3.10\"" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, @@ -1784,7 +1719,6 @@ version = "4.67.1" description = "Fast, Extensible Progress Meter" optional = false python-versions = ">=3.7" -groups = ["main", "dev"] files = [ {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, @@ -1806,7 +1740,6 @@ version = "4.15.0" description = "Twitter library for Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "tweepy-4.15.0-py3-none-any.whl", hash = "sha256:64adcea317158937059e4e2897b3ceb750b0c2dd5df58938c2da8f7eb3b88e6a"}, {file = "tweepy-4.15.0.tar.gz", hash = "sha256:1345cbcdf0a75e2d89f424c559fd49fda4d8cd7be25cd5131e3b57bad8a21d76"}, @@ -1830,12 +1763,10 @@ version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" -groups = ["main", "dev"] files = [ {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, ] -markers = {dev = "python_version == \"3.10\""} [[package]] name = "tzdata" @@ -1843,8 +1774,6 @@ version = "2025.1" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" -groups = ["main"] -markers = "sys_platform == \"win32\"" files = [ {file = "tzdata-2025.1-py2.py3-none-any.whl", hash = "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"}, {file = "tzdata-2025.1.tar.gz", hash = "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694"}, @@ -1856,7 +1785,6 @@ version = "4.1.1" description = "Implementation of RFC 6570 URI Templates" optional = false python-versions = ">=3.6" -groups = ["main"] files = [ {file = "uritemplate-4.1.1-py2.py3-none-any.whl", hash = "sha256:830c08b8d99bdd312ea4ead05994a38e8936266f84b9a7878232db50b044e02e"}, {file = "uritemplate-4.1.1.tar.gz", hash = "sha256:4346edfc5c3b79f694bccd6d6099a322bbeb628dbf2cd86eea55a456ce5124f0"}, @@ -1868,17 +1796,16 @@ version = "2.6.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f"}, {file = "urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1"}, ] [package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "uvicorn" @@ -1886,7 +1813,6 @@ version = "0.34.0" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"}, {file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"}, @@ -1898,7 +1824,7 @@ h11 = ">=0.8" typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] -standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +standard = ["colorama (>=0.4)", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] [[package]] name = "virtualenv" @@ -1906,7 +1832,6 @@ version = "20.29.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, @@ -1919,7 +1844,7 @@ platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "webencodings" @@ -1927,7 +1852,6 @@ version = "0.5.1" description = "Character encoding aliases for legacy web content" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"}, {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"}, @@ -1939,7 +1863,6 @@ version = "6.9.0" description = "Radically simplified static file serving for WSGI applications" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "whitenoise-6.9.0-py3-none-any.whl", hash = "sha256:c8a489049b7ee9889617bb4c274a153f3d979e8f51d2efd0f5b403caf41c57df"}, {file = "whitenoise-6.9.0.tar.gz", hash = "sha256:8c4a7c9d384694990c26f3047e118c691557481d624f069b7f7752a2f735d609"}, @@ -1949,6 +1872,6 @@ files = [ brotli = ["brotli"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = "^3.10" -content-hash = "637c808ca7e717a872f26b077fadf360e774dccb1b2944f66ee1c8ce2c5ec656" +content-hash = "4c8a566a1ac9600441de21101fe4f468bc19b8a335edc690131c78c54be0e659" diff --git a/pyproject.toml b/pyproject.toml index fde77eafb..74bf56b60 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -39,6 +39,7 @@ django-redis = "^5.4.0" redis = "^6.4.0" mysqlclient = "^2.2.4" psutil = "^7.1.3" +defusedxml = "^0.7.1" [tool.poetry.group.dev.dependencies] djlint = "^1.34.1" diff --git a/web/management/commands/cleanup_expired_messages.py b/web/management/commands/cleanup_expired_messages.py new file mode 100644 index 000000000..5c2f19d27 --- /dev/null +++ b/web/management/commands/cleanup_expired_messages.py @@ -0,0 +1,24 @@ +from typing import Any + +from django.core.management.base import BaseCommand + +from web.models import PeerMessage +from web.secure_messaging import MESSAGE_RETENTION_DAYS, get_message_retention_cutoff + + +class Command(BaseCommand): + """Management command to delete peer messages older than the retention period.""" + + help = f"Deletes direct messages older than {MESSAGE_RETENTION_DAYS} days." + + def handle(self, *args: Any, **options: Any) -> None: + """ + Execute the management command to remove expired messages. + + Deletes all PeerMessage records created before the retention cutoff + and reports the count of deleted messages to stdout. + """ + cutoff = get_message_retention_cutoff() + expired_qs = PeerMessage.objects.filter(created_at__lt=cutoff) + deleted_count, _ = expired_qs.delete() + self.stdout.write(self.style.SUCCESS(f"Successfully deleted {deleted_count} expired messages")) diff --git a/web/management/commands/run_daily.py b/web/management/commands/run_daily.py index 55c4441c5..a95bcf995 100644 --- a/web/management/commands/run_daily.py +++ b/web/management/commands/run_daily.py @@ -30,6 +30,11 @@ def handle(self, *args, **options): call_command("cleanup_abandoned_drafts") self.stdout.write(self.style.SUCCESS("Successfully completed cleanup_abandoned_drafts")) + # Clean up expired direct messages + self.stdout.write("Running cleanup_expired_messages...") + call_command("cleanup_expired_messages") + self.stdout.write(self.style.SUCCESS("Successfully completed cleanup_expired_messages")) + except Exception as e: self.stdout.write(self.style.ERROR(f"Error running daily tasks: {str(e)}")) raise e diff --git a/web/secure_messaging.py b/web/secure_messaging.py index 1fd0140ac..0422ca92e 100644 --- a/web/secure_messaging.py +++ b/web/secure_messaging.py @@ -1,16 +1,222 @@ +import base64 +import json +from datetime import timedelta +from html import unescape +from urllib.parse import urlparse + +import bleach from cryptography.fernet import Fernet +from defusedxml import ElementTree as SafeElementTree from django.conf import settings from django.contrib import messages from django.contrib.auth import get_user_model from django.contrib.auth.decorators import login_required from django.core.mail import send_mail +from django.db import models +from django.db.models import Q from django.http import HttpResponse, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.template.loader import render_to_string from django.utils import timezone +from django.views.decorators.http import require_POST from .models import PeerMessage +MESSAGE_RETENTION_DAYS = 7 + + +def sanitize_svg(svg_content: str) -> str: + """Sanitize SVG content using a DOM-based allowlist strategy.""" + if not svg_content: + return "" + + allowed_tags = { + "svg", + "g", + "path", + "circle", + "rect", + "ellipse", + "line", + "polyline", + "polygon", + "text", + "tspan", + "defs", + "linearGradient", + "radialGradient", + "stop", + "clipPath", + "mask", + "title", + "desc", + } + disallowed_tags = { + "script", + "iframe", + "object", + "embed", + "foreignObject", + "link", + "meta", + "base", + "style", + "animate", + "set", + "use", + } + allowed_attrs = { + "id", + "class", + "viewBox", + "width", + "height", + "x", + "y", + "cx", + "cy", + "r", + "rx", + "ry", + "d", + "fill", + "fill-rule", + "fill-opacity", + "stroke", + "stroke-width", + "stroke-linecap", + "stroke-linejoin", + "stroke-opacity", + "opacity", + "transform", + "preserveAspectRatio", + "points", + "x1", + "x2", + "y1", + "y2", + "gradientUnits", + "gradientTransform", + "offset", + "stop-color", + "stop-opacity", + "clip-path", + "mask", + "role", + "aria-label", + "focusable", + "xmlns", + "version", + "href", + "src", + } + + def local_name(name: str) -> str: + return name.split("}", 1)[1] if name.startswith("{") else name + + def is_safe_uri(value: str) -> bool: + decoded = unescape((value or "").strip()) + if not decoded: + return False + if decoded.startswith("#"): + return True + parsed = urlparse(decoded) + scheme = (parsed.scheme or "").lower() + if scheme in {"http", "https"}: + return True + return scheme == "data" and decoded.lower().startswith("data:image/") + + try: + root = SafeElementTree.fromstring(svg_content) + except Exception: + return "" + + if local_name(root.tag) != "svg": + return "" + + parent_map = {child: parent for parent in root.iter() for child in parent} + + for element in list(root.iter()): + tag_name = local_name(element.tag) + if tag_name in disallowed_tags or tag_name not in allowed_tags: + parent = parent_map.get(element) + if parent is not None: + parent.remove(element) + continue + + for attribute_name in list(element.attrib.keys()): + attr_name = local_name(attribute_name) + attr_name_lower = attr_name.lower() + + if attr_name_lower.startswith("on"): + element.attrib.pop(attribute_name, None) + continue + + if attr_name_lower in {"style", "xmlns:xlink", "xlink:href"}: + element.attrib.pop(attribute_name, None) + continue + + if attr_name not in allowed_attrs: + element.attrib.pop(attribute_name, None) + continue + + if attr_name in {"href", "src"} and not is_safe_uri(element.attrib.get(attribute_name, "")): + element.attrib.pop(attribute_name, None) + + serialized_svg = SafeElementTree.tostring(root, encoding="unicode") + cleaned_svg = bleach.clean( + serialized_svg, + tags=allowed_tags, + attributes={"*": list(allowed_attrs)}, + protocols=["http", "https", "data"], + strip=True, + ) + return cleaned_svg.strip() + + +@login_required +@require_POST +def mark_messages_read(request): + """ + AJAX endpoint to mark messages from a specific sender as read. + + Expects JSON body with 'username' field identifying the sender. + Updates is_read=True and read_at timestamp for matching messages. + + Returns: + JsonResponse with success status or error details. + """ + try: + data = json.loads(request.body) + except json.JSONDecodeError: + return JsonResponse({"success": False, "error": "Invalid JSON"}, status=400) + username = data.get("username") + if not username: + return JsonResponse({"success": False, "error": "No username provided"}, status=400) + User = get_user_model() + try: + sender = User.objects.get(username=username) + except User.DoesNotExist: + return JsonResponse({"success": False, "error": "User not found"}, status=404) + PeerMessage.objects.filter(sender=sender, receiver=request.user, is_read=False).update( + is_read=True, read_at=timezone.now() + ) + return JsonResponse({"success": True}) + + +def get_message_retention_cutoff(): + """Return the datetime cutoff for message retention based on MESSAGE_RETENTION_DAYS.""" + return timezone.now() - timedelta(days=MESSAGE_RETENTION_DAYS) + + +def cleanup_expired_peer_messages() -> int: + """Delete direct messages older than MESSAGE_RETENTION_DAYS and return deleted count.""" + cutoff = get_message_retention_cutoff() + expired_qs = PeerMessage.objects.filter(created_at__lt=cutoff) + deleted_count = expired_qs.delete()[0] + return deleted_count + + # Initialize Fernet with the master key from settings master_fernet = Fernet(settings.SECURE_MESSAGE_KEY) @@ -43,10 +249,12 @@ def decrypt_message_with_random_key(encrypted_message: str, encrypted_random_key # --- Simple Encryption Utility Functions (if needed) --- def encrypt_message(message: str) -> bytes: + """Encrypt a message using the master Fernet key.""" return master_fernet.encrypt(message.encode("utf-8")) def decrypt_message(token: bytes) -> str: + """Decrypt a Fernet-encrypted token using the master key.""" return master_fernet.decrypt(token).decode("utf-8") @@ -72,38 +280,113 @@ def messaging_dashboard(request): It immediately displays all messages (decrypted) for the logged-in user, marks them as read, and computes an expiration countdown (messages expire 7 days after creation). """ - messages_qs = PeerMessage.objects.filter(receiver=request.user).order_by("created_at") - message_list = [] - now = timezone.now() - for msg in messages_qs: - # Mark message as read and update read receipt - if not msg.is_read: - msg.is_read = True - msg.read_at = now - msg.save(update_fields=["is_read", "read_at"]) + from django.contrib.auth import get_user_model + + User = get_user_model() + cleanup_expired_peer_messages() + # Handle POST for sending a message from dashboard chat + if request.method == "POST": + recipient_identifier = request.POST.get("recipient") + message_text = request.POST.get("message") + if not recipient_identifier or not message_text: + messages.error(request, "Both recipient and message are required.") + return redirect("messaging_dashboard") try: - decrypted_message = decrypt_message_with_random_key(msg.content, msg.encrypted_key) - except Exception: - decrypted_message = "[Error decrypting message]" - expires_at = msg.created_at + timezone.timedelta(days=7) - time_remaining = expires_at - now - days = time_remaining.days - hours, remainder = divmod(time_remaining.seconds, 3600) - minutes, _ = divmod(remainder, 60) - expiration_str = f"{days}d {hours}h {minutes}m" - message_list.append( + recipient = User.objects.get(username=recipient_identifier) + except User.DoesNotExist: + messages.error(request, "Recipient not found.") + return redirect("messaging_dashboard") + encrypted_message, encrypted_key = encrypt_message_with_random_key(message_text) + PeerMessage.objects.create( + sender=request.user, receiver=recipient, content=encrypted_message, encrypted_key=encrypted_key + ) + messages.success(request, "Message sent successfully!") + return redirect("messaging_dashboard") + + # now = timezone.now() # Removed unused variable + # Get all users the current user has messaged or received from + sent_users = PeerMessage.objects.filter(sender=request.user).values_list("receiver", flat=True) + received_users = PeerMessage.objects.filter(receiver=request.user).values_list("sender", flat=True) + user_ids = set(list(sent_users) + list(received_users)) + user_ids.discard(request.user.id) + + users = User.objects.filter(id__in=user_ids).select_related("profile") + users_by_id = {user.id: user for user in users} + + all_messages = ( + PeerMessage.objects.filter( + models.Q(sender=request.user, receiver_id__in=user_ids) + | models.Q(receiver=request.user, sender_id__in=user_ids) + ) + .select_related("sender", "receiver") + .order_by("created_at") + ) + + messages_by_other_user_id = {} + current_user_id = request.user.id + for msg in all_messages: + other_user_id = msg.receiver_id if msg.sender_id == current_user_id else msg.sender_id + messages_by_other_user_id.setdefault(other_user_id, []).append(msg) + + unread_counts = dict( + PeerMessage.objects.filter(sender_id__in=user_ids, receiver=request.user, is_read=False) + .values_list("sender_id") + .annotate(unread=models.Count("id")) + ) + + people = [] + for uid in user_ids: + user = users_by_id.get(uid) + if not user: + continue + # Get avatar URL or fallback + avatar_url = None + if hasattr(user, "profile"): + if getattr(user.profile, "custom_avatar", None) and getattr(user.profile.custom_avatar, "svg", None): + # Custom SVG avatar (render as data URI) + svg = user.profile.custom_avatar.svg + sanitized_svg = sanitize_svg(svg) + if sanitized_svg: + encoded_svg = base64.b64encode(sanitized_svg.encode("utf-8")).decode("ascii") + avatar_url = f"data:image/svg+xml;base64,{encoded_svg}" + elif getattr(user.profile, "avatar", None): + if user.profile.avatar: + avatar_url = user.profile.avatar.url + + msgs = messages_by_other_user_id.get(uid, []) + msg_list = [] + for msg in msgs: + try: + decrypted_message = decrypt_message_with_random_key(msg.content, msg.encrypted_key) + except Exception: + decrypted_message = "[Error decrypting message]" + msg_list.append( + { + "id": msg.id, + "sender": msg.sender.username, + "content": decrypted_message, + "sent_at": msg.created_at, + "starred": msg.starred, + } + ) + has_unread = unread_counts.get(uid, 0) > 0 + people.append( { - "id": msg.id, - "sender": msg.sender.username, - "content": decrypted_message, - "sent_at": msg.created_at, - "expires_in": expiration_str, - "starred": msg.starred, + "username": user.username, + "display_name": user.get_full_name() or user.username, + "avatar_url": avatar_url, + "messages": msg_list, + "has_unread": has_unread, } ) + people = [person for person in people if person["messages"]] + people.sort(key=lambda person: person["messages"][-1]["sent_at"], reverse=True) + # For legacy: keep inbox_count for header + all_received = PeerMessage.objects.filter(receiver=request.user) + inbox_count = all_received.count() context = { - "messages": message_list, - "inbox_count": len(message_list), + "people": people, + "inbox_count": inbox_count, } return render(request, "web/messaging/dashboard.html", context) @@ -134,7 +417,7 @@ def compose_message(request): sender=request.user, receiver=recipient, content=encrypted_message, encrypted_key=encrypted_key ) messages.success(request, "Message sent successfully!") - return redirect("compose_message") + return redirect("messaging_dashboard") return render(request, "web/messaging/compose.html") @@ -165,45 +448,6 @@ def send_encrypted_message(request): return JsonResponse({"error": "Invalid method."}, status=405) -@login_required -def inbox(request): - """ - Renders an inbox page displaying decrypted messages for the logged-in user. - Also computes an expiration countdown (messages expire 7 days after creation). - """ - messages_qs = PeerMessage.objects.filter(receiver=request.user).order_by("created_at") - message_list = [] - now = timezone.now() - for msg in messages_qs: - # Mark message as read and update read receipt - if not msg.is_read: - msg.is_read = True - msg.read_at = now - msg.save(update_fields=["is_read", "read_at"]) - try: - decrypted_message = decrypt_message_with_random_key(msg.content, msg.encrypted_key) - except Exception: - decrypted_message = "[Error decrypting message]" - expires_at = msg.created_at + timezone.timedelta(days=7) - time_remaining = expires_at - now - days = time_remaining.days - hours, remainder = divmod(time_remaining.seconds, 3600) - minutes, _ = divmod(remainder, 60) - expiration_str = f"{days}d {hours}h {minutes}m" - message_list.append( - { - "id": msg.id, - "sender": msg.sender.username, - "content": decrypted_message, - "sent_at": msg.created_at.isoformat(), - "expires_in": expiration_str, - "starred": msg.starred, - "is_read": msg.is_read, - } - ) - return render(request, "web/peer/inbox.html", {"messages": message_list}) - - @login_required def download_message(request, message_id): """ @@ -224,8 +468,10 @@ def download_message(request, message_id): @login_required +@require_POST def toggle_star_message(request, message_id): - message = get_object_or_404(PeerMessage, id=message_id, receiver=request.user) + """Toggle the starred status of a message owned by the current user.""" + message = get_object_or_404(PeerMessage, Q(sender=request.user) | Q(receiver=request.user), id=message_id) message.starred = not message.starred message.save(update_fields=["starred"]) - return redirect("messaging_dashboard") + return JsonResponse({"success": True, "message_id": message.id, "starred": message.starred}) diff --git a/web/templates/base.html b/web/templates/base.html index 7afa700a9..5931bd046 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -355,7 +355,7 @@ - diff --git a/web/templates/web/messaging/dashboard.html b/web/templates/web/messaging/dashboard.html index 358e464ba..776f25967 100644 --- a/web/templates/web/messaging/dashboard.html +++ b/web/templates/web/messaging/dashboard.html @@ -5,60 +5,519 @@ {% endblock title %} {% block content %}
-
-

Your Inbox

-

- You have {{ inbox_count }} messages in your inbox - ({{ unread_count }} unread). -

-
+
+ +
-
- {% if messages %} -
    - {% for msg in messages %} -
  • -
    -

    From: {{ msg.sender }}

    -

    {{ msg.content }}

    -
    - Sent at: {{ msg.sent_at|date:"M d, Y h:ia" }} -
    - Expires in: {{ msg.expires_in }} +
    + +
    +

    + Messages are stored for 7 days only. +

    +
    + {% for person in people %} +
    -
    -
    - {% csrf_token %} - -
    -
    - {% csrf_token %} - -
    -
    -
  • + {% endif %} + {{ person.display_name }} + {% if person.has_unread %} + New + {% endif %} + {% endfor %} -
- {% else %} -

No messages in your inbox.

- {% endif %} +
+ + + + +
+ +
+ {# djlint:off #} + + {# djlint:on #} + {% endblock content %} diff --git a/web/templates/web/peer/inbox.html b/web/templates/web/peer/inbox.html index f7468e684..958c340be 100644 --- a/web/templates/web/peer/inbox.html +++ b/web/templates/web/peer/inbox.html @@ -10,45 +10,3 @@

Your Inbox< {% if messages %} - {% else %} -

No messages in your inbox.

- {% endif %} - - -{% endblock content %} diff --git a/web/templates/web/peer/messages.html b/web/templates/web/peer/messages.html index 1d42fc599..2b129861f 100644 --- a/web/templates/web/peer/messages.html +++ b/web/templates/web/peer/messages.html @@ -19,9 +19,6 @@

Messages

{% for message in messages %}
-
- {{ message.sender.get_full_name|default:message.sender.username }} -

{{ message.content }}

diff --git a/web/tests/test_securemessaging.py b/web/tests/test_securemessaging.py index 5f27e4834..76bc4e0d0 100644 --- a/web/tests/test_securemessaging.py +++ b/web/tests/test_securemessaging.py @@ -104,11 +104,12 @@ def test_dashboard_view_includes_messages(self): """ Test that the messaging dashboard view renders correctly with messages. """ + sender = User.objects.create_user(username="peer_user", email="peer@example.com", password="password") # Create a message for the user original_message = "Dashboard test message" encrypted_message, encrypted_key = encrypt_message_with_random_key(original_message) PeerMessage.objects.create( - sender=self.user, + sender=sender, receiver=self.user, content=encrypted_message, encrypted_key=encrypted_key, @@ -120,3 +121,22 @@ def test_dashboard_view_includes_messages(self): self.assertEqual(response.status_code, 200) # Check that the original plaintext appears in the rendered HTML self.assertContains(response, original_message) + + def test_dashboard_deletes_messages_older_than_seven_days(self): + """Messages older than 7 days are deleted on dashboard access.""" + sender = User.objects.create_user(username="sender", email="sender@example.com", password="password") + original_message = "Old message" + encrypted_message, encrypted_key = encrypt_message_with_random_key(original_message) + + msg = PeerMessage.objects.create( + sender=sender, + receiver=self.user, + content=encrypted_message, + encrypted_key=encrypted_key, + is_read=False, + ) + PeerMessage.objects.filter(id=msg.id).update(created_at=timezone.now() - datetime.timedelta(days=8)) + + response = self.client.get(reverse("messaging_dashboard")) + self.assertEqual(response.status_code, 200) + self.assertFalse(PeerMessage.objects.filter(id=msg.id).exists()) diff --git a/web/urls.py b/web/urls.py index 3fb4e298a..abb660133 100644 --- a/web/urls.py +++ b/web/urls.py @@ -10,7 +10,7 @@ from .secure_messaging import ( compose_message, download_message, - inbox, + mark_messages_read, messaging_dashboard, send_encrypted_message, toggle_star_message, @@ -121,9 +121,9 @@ path("teachers//message/", views.message_teacher, name="message_teacher"), path("sessions//duplicate/", views.duplicate_session, name="duplicate_session"), path("messaging/dashboard/", messaging_dashboard, name="messaging_dashboard"), + path("messaging/mark_read/", mark_messages_read, name="mark_messages_read"), path("messaging/compose/", compose_message, name="compose_message"), path("secure/send/", send_encrypted_message, name="send_encrypted_message"), - path("secure/inbox/", inbox, name="inbox"), path("secure/download//", download_message, name="download_message"), path("secure/toggle_star//", toggle_star_message, name="toggle_star_message"), # Virtual Lab Links