Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 4f29c5d35d 🐛(backend) fix unescaped dot in regex pattern
The dot before (?P<extension>...) was not escaped and matched any
character instead of a literal period.

Escape it to align with MEDIA_STORAGE_URL_PATTERN, which correctly
uses \. for the file extension separator.
2026-03-13 15:42:44 +01:00
199 changed files with 2250 additions and 7939 deletions
+33 -48
View File
@@ -12,9 +12,6 @@ on:
branches: branches:
- 'main' - 'main'
permissions:
contents: read
env: env:
DOCKER_USER: 1001:127 DOCKER_USER: 1001:127
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
@@ -23,8 +20,6 @@ env:
jobs: jobs:
build-and-push-backend: build-and-push-backend:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- -
name: Checkout repository name: Checkout repository
@@ -48,12 +43,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_HUB_USER }} username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }} password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- # -
name: Run trivy scan # name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main # uses: numerique-gouv/action-trivy-cache@main
with: # with:
docker-build-args: '--target backend-production -f Dockerfile' # docker-build-args: '--target backend-production -f Dockerfile'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}' # docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -68,8 +63,6 @@ jobs:
build-and-push-frontend-generic: build-and-push-frontend-generic:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- -
name: Checkout repository name: Checkout repository
@@ -93,12 +86,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_HUB_USER }} username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }} password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- # -
name: Run trivy scan # name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main # uses: numerique-gouv/action-trivy-cache@main
with: # with:
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production' # docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}' # docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -114,8 +107,6 @@ jobs:
build-and-push-frontend-dinum: build-and-push-frontend-dinum:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- -
name: Checkout repository name: Checkout repository
@@ -139,12 +130,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_HUB_USER }} username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }} password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- # -
name: Run trivy scan # name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main # uses: numerique-gouv/action-trivy-cache@main
with: # with:
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production' # docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}' # docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -160,8 +151,6 @@ jobs:
build-and-push-summary: build-and-push-summary:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- -
name: Checkout repository name: Checkout repository
@@ -185,13 +174,13 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_HUB_USER }} username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }} password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- # -
name: Run trivy scan # name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main # uses: numerique-gouv/action-trivy-cache@main
continue-on-error: true # continue-on-error: true
with: # with:
docker-build-args: '-f src/summary/Dockerfile --target production' # docker-build-args: '-f src/summary/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}' # docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
docker-context: './src/summary' docker-context: './src/summary'
- -
name: Build and push name: Build and push
@@ -208,8 +197,6 @@ jobs:
build-and-push-agents: build-and-push-agents:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions:
contents: read
steps: steps:
- -
name: Checkout repository name: Checkout repository
@@ -233,14 +220,14 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_HUB_USER }} username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }} password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- # -
name: Run trivy scan # name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main # uses: numerique-gouv/action-trivy-cache@main
continue-on-error: true # continue-on-error: true
with: # with:
docker-build-args: '-f src/agents/Dockerfile --target production' # docker-build-args: '-f src/agents/Dockerfile --target production'
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}' # docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
docker-context: './src/agents' # docker-context: './src/agents'
- -
name: Build and push name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
@@ -255,8 +242,6 @@ jobs:
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
notify-argocd: notify-argocd:
permissions:
contents: read
needs: needs:
- build-and-push-frontend-generic - build-and-push-frontend-generic
- build-and-push-frontend-dinum - build-and-push-frontend-dinum
+2 -39
View File
@@ -223,6 +223,8 @@ jobs:
DB_PORT: 5432 DB_PORT: 5432
REDIS_URL: redis://localhost:6379/1 REDIS_URL: redis://localhost:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
LIVEKIT_API_SECRET: secret
LIVEKIT_API_KEY: devkey
AWS_S3_ENDPOINT_URL: http://localhost:9000 AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_ACCESS_KEY_ID: meet AWS_S3_ACCESS_KEY_ID: meet
AWS_S3_SECRET_ACCESS_KEY: password AWS_S3_SECRET_ACCESS_KEY: password
@@ -295,45 +297,6 @@ jobs:
- name: Run tests - name: Run tests
run: uv run pytest -n 2 run: uv run pytest -n 2
test-summary:
runs-on: ubuntu-latest
permissions:
contents: read
defaults:
run:
working-directory: src/summary
env:
V1_TENANT_ID: 'test-tenant'
AUTHORIZED_TENANTS: '[{"id": "test-tenant", "api_key": "test-api-token", "webhook_url": "https://example.com/webhook", "webhook_api_key": "test-webhook-api-key"}]'
AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage"
AWS_S3_ENDPOINT_URL: "minio:9000"
AWS_S3_ACCESS_KEY_ID: "meet"
AWS_S3_SECRET_ACCESS_KEY: "password"
WHISPERX_BASE_URL: "https://configure-your-url.com"
WHISPERX_ASR_MODEL: "large-v2"
WHISPERX_API_KEY: "test-whisperx-secret"
WHISPERX_DEFAULT_LANGUAGE: "fr"
LLM_BASE_URL: "https://configure-your-url.com"
LLM_API_KEY: "test-llm-secret"
LLM_MODEL: "test-llm-model"
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
cache: "pip"
- name: Install development dependencies
run: pip install --user .[dev]
- name: Run summary tests
run: ~/.local/bin/pytest
lint-front: lint-front:
runs-on: ubuntu-latest runs-on: ubuntu-latest
permissions: permissions:
+2 -65
View File
@@ -10,84 +10,24 @@ and this project adheres to
### Added ### Added
- 🔒️(helm) Add pod and container securityContext #1197
- ✨(summary) add routes v2 for async STT and summary tasks #1171
- ✅(backend) add unit tests for JwtTokenService #1232
### Changed
- ⬆️(backend) bump lodash from 4.17.23 to 4.18.1 in /src/mail
### Fixed
- ⬆️(dependencies) update aiohttp to v3.13.4 [SECURITY]
- ⬆️(dependencies) update vite to v7.3.2 [SECURITY]
## [1.13.0] - 2026-03-31
### Changed
- ⬆️(dependencies) update python dependencies
- ♿️(frontend) add explicit region for call controls #1216
- ♿️(frontend) improve accessibility of the reaction toolbar #1216
- ♿️(frontend) enhance sidepanel navigation accessibility #1216
### Fixed
- 🔒️(backend) fix email disclosure in room invitation endpoint #1200
- 🐛(backend) fix regression in update-participant endpoint #1204
## [1.12.0] - 2026-03-24
### Changed
- ♻️(backend) configurable SESSION_ENGINE #1038 #1154
- ♿️(frontend) fix sidepanel accessibility aria-label #1182
- ♿️(frontend) fix more tools heading hierarchy #1181
- ♿️(fronted) improve button descriptions for More tools actions #1184
- 💄(spinner) enforce spinner height #1183
- 💄(custom-background) add upload indicator with preview #1183
- ♿️(backend) improve logo accessibility in recording email notification #1092
- ♿️(summary) improve accessibility of transcription download link #1187
- 💄(frontend) show OS-specific shortcut in participant tile hint #1193
- ⬆️(frontend) bump flatted from 3.3.1 to 3.4.2 in /src/frontend #1188
- ⬆️(frontend) bump undici from 6.23.0 to 6.24.1 in /src/frontend
- ⬆️(frontend) bump hono from 4.12.2 to 4.12.7 in /src/frontend
- ⬆️(frontend) bump dompurify from 3.3.1 to 3.3.2 in /src/frontend
### Fixed
- 🐛(frontend) disable personal custom background while deleting #1183
- 🐛(frontend) auto-select new custom background when not logged in #1183
- 🐛(frontend) fix device selection not applying during conference #1156
## [1.11.0] - 2026-03-19
### Added
- ✨(helm) support celery with our Django backend #1124 - ✨(helm) support celery with our Django backend #1124
- ✨(helm) support ingress for custom background image #1124 - ✨(helm) support ingress for custom background image #1124
- ✨(backend) add authenticated user rate throttling on request-entry #1129 - ✨(backend) add authenticated user rate throttling on request-entry #1129
- ✨(backend) expose `is_active` field for Application in Django admin #1133 - ✨(backend) expose `is_active` field for Application in Django admin #1133
- ✨(file-upload) disable by default & limit count by user #1141 - ✨(file-upload) disable by default & limit count by user #1141
- ✨(frontend) custom background #1067
### Changed ### Changed
- ♿️(frontend) Caption text size setting for accessibility #1062 - ♿️(frontend) Caption text size setting for accessibility #1062
- ♿️(frontend) sync html lang attribute with i18n for screen readers #1111 - ♿️(frontend) sync html lang attribute with i18n for screen readers #1111
- ♿️(frontend) improve MoreLink a11y and UX on home page #1112 - ♿️(frontend) improve MoreLink a11y and UX on home page #1112
-(frontend) improve chat toast a11y for screen readers #1109 - ♿(frontend) improve chat toast a11y for screen readers #1109
-(frontend) improve ui and aria labels for help article links #1108 - ♿(frontend) improve ui and aria labels for help article links #1108
- 🌐(frontend) improve German translation #1125 - 🌐(frontend) improve German translation #1125
- 🔨(python-env) migrate meet main app to UV #1120 - 🔨(python-env) migrate meet main app to UV #1120
- ♻️(backend) align Application model field with `is_active` convention #1133 - ♻️(backend) align Application model field with `is_active` convention #1133
- 🔐(backend) avoids revealing the inactive status of an application #1135 - 🔐(backend) avoids revealing the inactive status of an application #1135
- ⚡️(helm) reduce initialDelaySeconds and add periods seconds #1139 - ⚡️(helm) reduce initialDelaySeconds and add periods seconds #1139
- 🔒️(backend) avoid information exposure through exception messages #1144
- ⬆️(dependencies) update PyJWT to v2.12.0 [SECURITY] #1151
- 📌(agents) unpin OpenSSL and related dependencies #1167
- ♿️(frontend) add caption font and background color customization #1122
### Fixed ### Fixed
@@ -95,7 +35,6 @@ and this project adheres to
- 🩹(backend) add page_size to pagination for room endpoints #1131 - 🩹(backend) add page_size to pagination for room endpoints #1131
- 🐛(backend) refactor lobby throttling to use participant id #1129 - 🐛(backend) refactor lobby throttling to use participant id #1129
- 🩹(backend) ignore non-recording uploads in storage webhook handler #1142 - 🩹(backend) ignore non-recording uploads in storage webhook handler #1142
- 🐛(frontend) fix dimension mismatch in BackgroundCustomProcessor #1116
## [1.10.0] - 2026-03-05 ## [1.10.0] - 2026-03-05
@@ -180,8 +119,6 @@ and this project adheres to
- ✨(backend) monitor throttling rate failure through sentry #964 - ✨(backend) monitor throttling rate failure through sentry #964
- 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957 - 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957
- ✨(feat) Introduce Picture-in-Picture (PiP) #890
### Changed ### Changed
+5 -11
View File
@@ -73,8 +73,7 @@ create-env-files: \
env.d/development/crowdin \ env.d/development/crowdin \
env.d/development/postgresql \ env.d/development/postgresql \
env.d/development/kc_postgresql \ env.d/development/kc_postgresql \
env.d/development/summary \ env.d/development/summary
env.d/development/kube-secret
.PHONY: create-env-files .PHONY: create-env-files
bootstrap: ## Prepare Docker images for the project bootstrap: ## Prepare Docker images for the project
@@ -192,7 +191,6 @@ lint-pylint: ## lint back-end python sources with pylint only on changed files f
test: ## run project tests test: ## run project tests
@$(MAKE) test-back-parallel @$(MAKE) test-back-parallel
@$(MAKE) test-summary
.PHONY: test .PHONY: test
test-back: ## run back-end tests test-back: ## run back-end tests
@@ -205,11 +203,6 @@ test-back-parallel: ## run all back-end tests in parallel
bin/pytest -n auto $${args:-${1}} bin/pytest -n auto $${args:-${1}}
.PHONY: test-back-parallel .PHONY: test-back-parallel
test-summary: ## run summary tests
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
bin/pytest-summary $${args:-${1}}
.PHONY: test-summary
makemigrations: ## run django makemigrations for the Meet project. makemigrations: ## run django makemigrations for the Meet project.
@echo "$(BOLD)Running makemigrations$(RESET)" @echo "$(BOLD)Running makemigrations$(RESET)"
@$(COMPOSE) up -d postgresql @$(COMPOSE) up -d postgresql
@@ -266,9 +259,6 @@ env.d/development/kc_postgresql:
env.d/development/summary: env.d/development/summary:
cp -n env.d/development/summary.dist env.d/development/summary cp -n env.d/development/summary.dist env.d/development/summary
env.d/development/kube-secret:
cp -n env.d/development/kube-secret.dist env.d/development/kube-secret
# -- Internationalization # -- Internationalization
env.d/development/crowdin: env.d/development/crowdin:
@@ -363,6 +353,10 @@ install-external-secrets: ## install the kubernetes secrets from Vaultwarden
./bin/install-external-secrets.sh ./bin/install-external-secrets.sh
.PHONY: build-k8s-cluster .PHONY: build-k8s-cluster
start-tilt: ## start the kubernetes cluster using kind
tilt up --namespace=meet -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
DEV_ENV=dev-keycloak tilt up --namespace=meet -f ./bin/Tiltfile DEV_ENV=dev-keycloak tilt up --namespace=meet -f ./bin/Tiltfile
.PHONY: build-k8s-cluster .PHONY: build-k8s-cluster
+2 -9
View File
@@ -2,7 +2,7 @@ load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
load('ext://namespace', 'namespace_create', 'namespace_inject') load('ext://namespace', 'namespace_create', 'namespace_inject')
namespace_create('meet') namespace_create('meet')
DEV_ENV = os.getenv('DEV_ENV', 'dev-keycloak') DEV_ENV = os.getenv('DEV_ENV', 'dev')
if DEV_ENV == 'dev-dinum': if DEV_ENV == 'dev-dinum':
update_settings(suppress_unused_image_warnings=["localhost:5001/meet-frontend-generic:latest"]) update_settings(suppress_unused_image_warnings=["localhost:5001/meet-frontend-generic:latest"])
@@ -95,19 +95,12 @@ docker_build(
) )
clean_old_images('localhost:5001/meet-livekit') clean_old_images('localhost:5001/meet-livekit')
load('ext://secret', 'secret_yaml_generic') k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev} template .'))
k8s_yaml(secret_yaml_generic(
name="secret-dev",
from_env_file="../env.d/development/kube-secret"
))
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .'))
k8s_resource('minio-bucket', resource_deps=['minio']) k8s_resource('minio-bucket', resource_deps=['minio'])
k8s_resource('meet-backend', resource_deps=['postgresql', 'minio', 'redis', 'livekit-livekit-server']) k8s_resource('meet-backend', resource_deps=['postgresql', 'minio', 'redis', 'livekit-livekit-server'])
k8s_resource('meet-celery-backend', resource_deps=['redis']) k8s_resource('meet-celery-backend', resource_deps=['redis'])
k8s_resource('meet-celery-summarize', resource_deps=['redis']) k8s_resource('meet-celery-summarize', resource_deps=['redis'])
k8s_resource('meet-celery-summary-backend', resource_deps=['redis'])
k8s_resource('meet-celery-transcribe', resource_deps=['redis']) k8s_resource('meet-celery-transcribe', resource_deps=['redis'])
k8s_resource('meet-backend-migrate', resource_deps=['meet-backend']) k8s_resource('meet-backend-migrate', resource_deps=['meet-backend'])
k8s_resource('livekit-livekit-server', resource_deps=['redis']) k8s_resource('livekit-livekit-server', resource_deps=['redis'])
-7
View File
@@ -1,7 +0,0 @@
#!/usr/bin/env bash
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
_dc_run \
app-summary-dev \
python -m pytest "$@"
+4 -3
View File
@@ -61,10 +61,11 @@ services:
`docker compose up -d` `docker compose up -d`
``` ```
Your keycloak instance is now available on https://id.yourdomain.tld Your keycloak instance is now available on https://doc.yourdomain.tld
> [!CAUTION] > [!CAUTION]
> Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image. You can find available versions on [Keycloak registry](https://quay.io/repository/keycloak/keycloak?tab=tags). > Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image. You can find available versions on [Keycloak registry](https://quay.io/repository/keycloak/keycloak?tab=tags).
```
## Creating an OIDC Client for Meet Application ## Creating an OIDC Client for Meet Application
@@ -75,7 +76,7 @@ Your keycloak instance is now available on https://id.yourdomain.tld
3. Enter the name of the realm - `meet`. 3. Enter the name of the realm - `meet`.
4. Click "Create". 4. Click "Create".
### Step 2: Create a New Client #### Step 2: Create a New Client
1. Navigate to the "Clients" tab. 1. Navigate to the "Clients" tab.
2. Click on the "Create client" button. 2. Click on the "Create client" button.
@@ -85,7 +86,7 @@ Your keycloak instance is now available on https://id.yourdomain.tld
1. Set the "Web Origins" to the URL of your meet application - e.g. `https://meet.example.com`. 1. Set the "Web Origins" to the URL of your meet application - e.g. `https://meet.example.com`.
1. Click "Save". 1. Click "Save".
### Step 3: Get Client Credentials #### Step 3: Get Client Credentials
1. Go to the "Credentials" tab. 1. Go to the "Credentials" tab.
2. Copy the client ID (`meet` in this example) and the client secret. 2. Copy the client ID (`meet` in this example) and the client secret.
-1
View File
@@ -190,7 +190,6 @@ paths:
'403': '403':
$ref: '#/components/responses/ForbiddenError' $ref: '#/components/responses/ForbiddenError'
/rooms/:
post: post:
tags: tags:
- Rooms - Rooms
-1
View File
@@ -113,7 +113,6 @@ paths:
'403': '403':
$ref: '#/components/responses/ForbiddenError' $ref: '#/components/responses/ForbiddenError'
/rooms/:
post: post:
tags: tags:
- Rooms - Rooms
+1 -1
View File
@@ -27,7 +27,7 @@ AWS_S3_DOMAIN_REPLACE=http://localhost:9000
AWS_S3_ENDPOINT_URL=http://minio:9000 AWS_S3_ENDPOINT_URL=http://minio:9000
AWS_S3_ACCESS_KEY_ID=meet AWS_S3_ACCESS_KEY_ID=meet
AWS_S3_SECRET_ACCESS_KEY=password AWS_S3_SECRET_ACCESS_KEY=password
MEDIA_BASE_URL=http://localhost:3000 MEDIA_BASE_URL=http://localhost:8083
FILE_UPLOAD_ENABLED=True FILE_UPLOAD_ENABLED=True
# OIDC # OIDC
-4
View File
@@ -1,4 +0,0 @@
WHISPERX_BASE_URL=https://configure-your-url.com
WHISPERX_API_KEY=<key>
LLM_BASE_URL=https://configure-your-url.com
LLM_API_KEY=<key>
-6
View File
@@ -36,12 +36,6 @@
"matchPackageNames": ["django"], "matchPackageNames": ["django"],
"allowedVersions": "<6.0.0" "allowedVersions": "<6.0.0"
}, },
{
"groupName": "allowed brevo versions",
"matchManagers": ["pep621"],
"matchPackageNames": ["brevo-python"],
"allowedVersions": "<3.0.0"
},
{ {
"enabled": false, "enabled": false,
"groupName": "ignored js dependencies", "groupName": "ignored js dependencies",
+2
View File
@@ -4,6 +4,8 @@ FROM python:3.13-slim AS base
RUN apt-get update && apt-get install -y \ RUN apt-get update && apt-get install -y \
libglib2.0-0 \ libglib2.0-0 \
libgobject-2.0-0 \ libgobject-2.0-0 \
"openssl=3.5.4-1~deb13u2" \
"libssl3t64=3.5.4-1~deb13u2" \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
FROM base AS builder FROM base AS builder
+6 -6
View File
@@ -1,20 +1,20 @@
[project] [project]
name = "agents" name = "agents"
version = "1.13.0" version = "1.10.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"livekit-agents==1.4.5", "livekit-agents==1.3.10",
"livekit-plugins-deepgram==1.4.5", "livekit-plugins-deepgram==1.3.10",
"livekit-plugins-silero==1.4.5", "livekit-plugins-silero==1.3.10",
"livekit-plugins-kyutai-lasuite==0.0.6", "livekit-plugins-kyutai-lasuite==0.0.6",
"python-dotenv==1.2.2", "python-dotenv==1.2.1",
"protobuf==6.33.5" "protobuf==6.33.5"
] ]
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"ruff==0.15.6", "ruff==0.14.4",
] ]
[build-system] [build-system]
+11 -4
View File
@@ -303,9 +303,6 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
) )
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
class ParticipantPermission(BaseModel): class ParticipantPermission(BaseModel):
"""Mirror the LiveKit ParticipantPermission protobuf. """Mirror the LiveKit ParticipantPermission protobuf.
@@ -316,7 +313,9 @@ class ParticipantPermission(BaseModel):
can_subscribe: bool | None = None can_subscribe: bool | None = None
can_publish: bool | None = None can_publish: bool | None = None
can_publish_data: bool | None = None can_publish_data: bool | None = None
can_publish_sources: list[TrackSource] = Field(default_factory=list) can_publish_sources: list[int] = Field(
default_factory=list
) # TrackSource enum values
hidden: bool | None = None hidden: bool | None = None
recorder: bool | None = None recorder: bool | None = None
can_update_metadata: bool | None = None can_update_metadata: bool | None = None
@@ -367,6 +366,14 @@ class UpdateParticipantSerializer(BaseParticipantsManagementSerializer):
f"Setting the following participant permissions is not allowed: " f"Setting the following participant permissions is not allowed: "
f"{', '.join(suspicious_fields)}." f"{', '.join(suspicious_fields)}."
) )
if permission.can_subscribe_metrics is not None:
raise serializers.ValidationError(
{
"permission": {
"can_subscribe_metrics": "This permission is not implemented."
}
}
)
return permission return permission
+5 -3
View File
@@ -488,7 +488,9 @@ class RoomViewSet(
if status_code == drf_status.HTTP_500_INTERNAL_SERVER_ERROR: if status_code == drf_status.HTTP_500_INTERNAL_SERVER_ERROR:
raise e raise e
return drf_response.Response({"status": "error"}, status=status_code) return drf_response.Response(
{"status": "error", "message": str(e)}, status=status_code
)
@decorators.action( @decorators.action(
detail=False, detail=False,
@@ -755,10 +757,10 @@ class RecordingViewSet(
recording_id = parser.get_recording_id(request.data) recording_id = parser.get_recording_id(request.data)
except ParsingEventDataError as e: except ParsingEventDataError as e:
raise drf_exceptions.PermissionDenied("Invalid request data.") from e raise drf_exceptions.PermissionDenied(f"Invalid request data: {e}") from e
except InvalidBucketError as e: except InvalidBucketError as e:
raise drf_exceptions.PermissionDenied("Invalid bucket specified.") from e raise drf_exceptions.PermissionDenied("Invalid bucket specified") from e
except InvalidFilepathError: except InvalidFilepathError:
return drf_response.Response( return drf_response.Response(
+1 -1
View File
@@ -14,7 +14,7 @@ FILE_EXT_REGEX = r"[a-zA-Z0-9]{1,10}"
# pylint: disable=line-too-long # pylint: disable=line-too-long
RECORDING_STORAGE_URL_PATTERN = re.compile( RECORDING_STORAGE_URL_PATTERN = re.compile(
rf"{settings.MEDIA_URL:s}{settings.RECORDING_OUTPUT_FOLDER}/(?P<recording_id>{UUID_REGEX:s})\.(?P<extension>{FILE_EXT_REGEX:s})" f"{settings.MEDIA_URL:s}{settings.RECORDING_OUTPUT_FOLDER}/(?P<recording_id>{UUID_REGEX:s})\.(?P<extension>{FILE_EXT_REGEX:s})"
) )
MEDIA_STORAGE_URL_PATTERN = re.compile( MEDIA_STORAGE_URL_PATTERN = re.compile(
+10 -13
View File
@@ -4,7 +4,7 @@ import smtplib
from logging import getLogger from logging import getLogger
from django.conf import settings from django.conf import settings
from django.core.mail import EmailMultiAlternatives from django.core.mail import send_mail
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import get_language, override from django.utils.translation import get_language, override
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -45,18 +45,15 @@ class InvitationService:
) )
) # Force translation ) # Force translation
email = EmailMultiAlternatives(
subject=subject,
body=msg_plain,
from_email=settings.EMAIL_FROM,
to=[],
bcc=emails,
)
email.attach_alternative(msg_html, "text/html")
try: try:
email.send() send_mail(
subject,
msg_plain,
settings.EMAIL_FROM,
emails,
html_message=msg_html,
fail_silently=False,
)
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
logger.error("invitations were not sent: %s", e) logger.error("invitation to %s was not sent: %s", emails, e)
raise InvitationError("Could not send invitation") from e raise InvitationError("Could not send invitation") from e
@@ -95,7 +95,7 @@ def test_save_recording_parsing_error(recording_settings, mock_get_parser, clien
) )
assert response.status_code == 403 assert response.status_code == 403
assert response.json() == {"detail": "Invalid request data."} assert response.json() == {"detail": "Invalid request data: Error message"}
def test_save_recording_bucket_error(recording_settings, mock_get_parser, client): def test_save_recording_bucket_error(recording_settings, mock_get_parser, client):
@@ -112,7 +112,7 @@ def test_save_recording_bucket_error(recording_settings, mock_get_parser, client
) )
assert response.status_code == 403 assert response.status_code == 403
assert response.json() == {"detail": "Invalid bucket specified."} assert response.json() == {"detail": "Invalid bucket specified"}
def test_save_recording_filetype_error(recording_settings, mock_get_parser): def test_save_recording_filetype_error(recording_settings, mock_get_parser):
@@ -240,9 +240,10 @@ def test_api_rooms_invite_error(mock_invite_to_room):
mock_invite_to_room.assert_called_once() mock_invite_to_room.assert_called_once()
@mock.patch("core.services.invitation.EmailMultiAlternatives") @mock.patch("core.services.invitation.send_mail")
def test_api_rooms_invite_success(mock_email_class, settings): def test_api_rooms_invite_success(mock_send_mail, settings):
"""Test privileged users should successfully send invitation emails.""" """Test privileged users should successfully send invitation emails."""
settings.EMAIL_BRAND_NAME = "ACME" settings.EMAIL_BRAND_NAME = "ACME"
settings.EMAIL_LOGO_IMG = "https://acme.com/logo" settings.EMAIL_LOGO_IMG = "https://acme.com/logo"
settings.EMAIL_APP_BASE_URL = "https://acme.com" settings.EMAIL_APP_BASE_URL = "https://acme.com"
@@ -254,6 +255,7 @@ def test_api_rooms_invite_success(mock_email_class, settings):
user = UserFactory() user = UserFactory()
room.accesses.create(user=user, role=random.choice(["administrator", "owner"])) room.accesses.create(user=user, role=random.choice(["administrator", "owner"]))
client.force_login(user) client.force_login(user)
data = {"emails": ["fabien@yopmail.com", "gerald@yopmail.com"]} data = {"emails": ["fabien@yopmail.com", "gerald@yopmail.com"]}
@@ -267,38 +269,26 @@ def test_api_rooms_invite_success(mock_email_class, settings):
assert response.status_code == 200 assert response.status_code == 200
assert response.json() == {"status": "success", "message": "invitations sent"} assert response.json() == {"status": "success", "message": "invitations sent"}
mock_email_class.assert_called_once() mock_send_mail.assert_called_once()
# Check constructor arguments subject, body, sender, recipients = mock_send_mail.call_args[0]
call_kwargs = mock_email_class.call_args[1] # EmailMultiAlternatives(**kwargs)
assert call_kwargs["subject"] == ( assert (
f"Video call in progress: {user.email} is waiting for you to connect" subject == f"Video call in progress: {user.email} is waiting for you to connect"
)
assert call_kwargs["from_email"] == "notifications@acme.com"
assert call_kwargs["to"] == []
assert sorted(call_kwargs["bcc"]) == sorted(
["fabien@yopmail.com", "gerald@yopmail.com"]
) )
# Check plain text body # Verify email contains expected content
plain_body = call_kwargs["body"]
required_content = [ required_content = [
"ACME", "ACME", # Brand name
"https://acme.com/logo", "https://acme.com/logo", # Logo URL
f"https://acme.com/{room.slug}", f"https://acme.com/{room.slug}", # Room url
f"acme.com/{room.slug}", f"acme.com/{room.slug}", # Room link
] ]
for content in required_content:
assert content in plain_body
# Check HTML alternative was attached
mock_instance = mock_email_class.return_value
mock_instance.attach_alternative.assert_called_once()
html_body, mimetype = mock_instance.attach_alternative.call_args[0]
assert mimetype == "text/html"
for content in required_content: for content in required_content:
assert content in html_body assert content in body
# Check send was called assert sender == "notifications@acme.com"
mock_instance.send.assert_called_once()
# Verify all owners received the email (order-independent comparison)
assert sorted(recipients) == sorted(["fabien@yopmail.com", "gerald@yopmail.com"])
@@ -130,11 +130,10 @@ def test_update_participant_success(mock_livekit_client):
"can_publish": True, "can_publish": True,
"can_publish_data": True, "can_publish_data": True,
"can_publish_sources": [ "can_publish_sources": [
"CAMERA", 1,
"MICROPHONE", 2,
], ], # [TrackSource.CAMERA, TrackSource.MICROPHONE]
"can_update_metadata": True, "can_update_metadata": True,
"can_subscribe_metrics": True,
}, },
"name": "John Doe", "name": "John Doe",
} }
@@ -156,14 +155,8 @@ def test_update_participant_success(mock_livekit_client):
{"can_subscribe": True}, {"can_subscribe": True},
{"can_publish": True}, {"can_publish": True},
{"can_publish_data": True}, {"can_publish_data": True},
{ {"can_publish_sources": [1, 2]},
"can_publish_sources": [
"CAMERA",
"MICROPHONE",
]
},
{"can_update_metadata": True}, {"can_update_metadata": True},
{"can_subscribe_metrics": False},
], ],
) )
def test_update_participant_permission_fields_are_optional( def test_update_participant_permission_fields_are_optional(
@@ -271,6 +264,35 @@ def test_update_participant_suspicious_permission_multiple(mock_suspicious):
) )
@pytest.mark.parametrize("value", (False, True))
def test_update_participant_unimplemented_can_subscribe_metrics(value):
"""Test update participant raises 400 when can_subscribe_metrics is set."""
client = APIClient()
room = RoomFactory()
user = UserFactory()
UserResourceAccessFactory(
resource=room, user=user, role=random.choice(["administrator", "owner"])
)
client.force_authenticate(user=user)
payload = {
"participant_identity": str(uuid4()),
"permission": {
"can_subscribe": True,
"can_publish": True,
"can_publish_data": True,
"can_update_metadata": False,
"can_subscribe_metrics": value,
},
}
url = reverse("rooms-update-participant", kwargs={"pk": room.id})
response = client.post(url, payload, format="json")
assert response.status_code == status.HTTP_400_BAD_REQUEST
assert "can_subscribe_metrics" in str(response.data)
def test_update_participant_forbidden_without_access(): def test_update_participant_forbidden_without_access():
"""Test update participant returns 403 when user lacks room privileges.""" """Test update participant returns 403 when user lacks room privileges."""
client = APIClient() client = APIClient()
@@ -205,7 +205,7 @@ def test_start_subtitle_wrong_signature(settings, mock_livekit_token):
"""Test that tokens signed with incorrect signature are rejected.""" """Test that tokens signed with incorrect signature are rejected."""
settings.ROOM_SUBTITLE_ENABLED = True settings.ROOM_SUBTITLE_ENABLED = True
settings.LIVEKIT_CONFIGURATION["api_secret"] = "wrong-secret-padded-to-32-bytes!!" settings.LIVEKIT_CONFIGURATION["api_secret"] = "wrong-secret"
room = RoomFactory() room = RoomFactory()
client = APIClient() client = APIClient()
@@ -47,7 +47,7 @@ def mock_livekit_config(settings):
"""Mock LiveKit configuration.""" """Mock LiveKit configuration."""
settings.LIVEKIT_CONFIGURATION = { settings.LIVEKIT_CONFIGURATION = {
"api_key": "test_api_key", "api_key": "test_api_key",
"api_secret": "test_api_secret_padded_to_32bytes!", "api_secret": "test_api_secret",
"url": "https://test-livekit.example.com/", "url": "https://test-livekit.example.com/",
} }
return settings.LIVEKIT_CONFIGURATION return settings.LIVEKIT_CONFIGURATION
@@ -77,6 +77,7 @@ def test_missing_auth_header(client, serialized_event_data, mock_livekit_config)
assert response.status_code == 401 assert response.status_code == 401
assert response.json() == { assert response.json() == {
"status": "error", "status": "error",
"message": "Authorization header missing",
} }
@@ -90,7 +91,7 @@ def test_invalid_payload(client, auth_token, mock_livekit_config):
) )
assert response.status_code == 400 assert response.status_code == 400
assert response.json() == {"status": "error"} assert response.json() == {"status": "error", "message": "Invalid webhook payload"}
def test_unknown_event_type(client, mock_livekit_config): def test_unknown_event_type(client, mock_livekit_config):
@@ -115,6 +116,7 @@ def test_unknown_event_type(client, mock_livekit_config):
assert response.status_code == 422 assert response.status_code == 422
assert response.json() == { assert response.json() == {
"status": "error", "status": "error",
"message": "Unknown webhook type: unknown_event_type",
} }
@@ -1,541 +0,0 @@
"""
Tests for JWT token service.
"""
# pylint: disable=W0212,W0621
import uuid
from unittest import mock
from django.core.exceptions import ImproperlyConfigured
import jwt as pyjwt
import pytest
from freezegun import freeze_time
from core.services.jwt_token import (
JwtTokenService,
TokenDecodeError,
TokenExpiredError,
TokenInvalidError,
)
# -- Fixtures --
@pytest.fixture
def jwt_service():
"""Create a JWT token service for testing."""
return JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="test-issuer",
audience="test-audience",
expiration_seconds=3600,
token_type="Bearer",
)
@pytest.fixture
def mock_user():
"""Create a mock user with a string ID."""
user = mock.Mock()
user.id = "test-user-id"
return user
# -- __init__ / Configuration --
def test_init_missing_secret_key():
"""Missing secret key should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Secret key is required"):
JwtTokenService(
secret_key="",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
def test_init_none_secret_key():
"""None secret key should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Secret key is required"):
JwtTokenService(
secret_key=None,
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
def test_init_missing_algorithm():
"""Missing algorithm should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Algorithm is required"):
JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
def test_init_none_algorithm():
"""None algorithm should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Algorithm is required"):
JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm=None,
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
def test_init_missing_token_type():
"""Missing token type should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Token's type is required"):
JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="",
)
def test_init_none_token_type():
"""None token type should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Token's type is required"):
JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type=None,
)
def test_init_none_expiration_seconds():
"""None expiration seconds should raise ImproperlyConfigured."""
with pytest.raises(ImproperlyConfigured, match="Expiration's seconds is required"):
JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=None,
token_type="Bearer",
)
def test_init_zero_expiration_seconds_is_accepted():
"""expiration_seconds=0 is falsy but should be accepted — token expires immediately."""
service = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=0,
token_type="Bearer",
)
assert service._expiration_seconds == 0
def test_init_stores_config_correctly():
"""All config values should be stored correctly on the instance."""
service = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="my-issuer",
audience="my-audience",
expiration_seconds=1800,
token_type="Bearer",
)
assert service._key == "test-secret-padded-to-32-bytes!!"
assert service._algorithm == "HS256"
assert service._issuer == "my-issuer"
assert service._audience == "my-audience"
assert service._expiration_seconds == 1800
assert service._token_type == "Bearer"
# -- generate_jwt / Return shape --
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_always_returns_required_keys(jwt_service, mock_user):
"""Response always contains access_token, token_type, and expires_in."""
result = jwt_service.generate_jwt(mock_user, scope="read")
assert "access_token" in result
assert "token_type" in result
assert "expires_in" in result
assert result["token_type"] == "Bearer"
assert result["expires_in"] == 3600
assert isinstance(result["access_token"], str)
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_scope_present_when_provided(jwt_service, mock_user):
"""scope key should be present in response when scope is provided."""
result = jwt_service.generate_jwt(mock_user, scope="read write")
assert result["scope"] == "read write"
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_scope_absent_when_empty(jwt_service, mock_user):
"""scope key should be absent from response when scope is empty."""
result = jwt_service.generate_jwt(mock_user, scope="")
assert "scope" not in result
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_scope_absent_when_none(jwt_service, mock_user):
"""scope key should be absent from response when scope is None."""
result = jwt_service.generate_jwt(mock_user, scope=None)
assert "scope" not in result
# -- generate_jwt / Payload correctness --
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_payload_contains_required_claims(jwt_service, mock_user):
"""Payload should always contain iat, exp, and user_id."""
result = jwt_service.generate_jwt(mock_user, scope="read")
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["iat"] == 1673784000
assert payload["exp"] == 1673787600
assert payload["user_id"] == "test-user-id"
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_exp_is_now_plus_expiration_seconds(mock_user):
"""exp should equal iat + expiration_seconds exactly."""
service = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=900,
token_type="Bearer",
)
result = service.generate_jwt(mock_user, scope="read")
payload = service.decode_jwt(result["access_token"])
assert payload["exp"] - payload["iat"] == 900
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_iss_included_when_set(jwt_service, mock_user):
"""iss should be present in payload when issuer is non-empty."""
result = jwt_service.generate_jwt(mock_user, scope="read")
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["iss"] == "test-issuer"
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_aud_included_when_set(jwt_service, mock_user):
"""aud should be present in payload when audience is non-empty."""
result = jwt_service.generate_jwt(mock_user, scope="read")
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["aud"] == "test-audience"
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_iss_absent_when_empty(mock_user):
"""iss should be absent from payload when issuer is empty string."""
service = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="",
audience="",
expiration_seconds=3600,
token_type="Bearer",
)
result = service.generate_jwt(mock_user, scope="read")
payload = pyjwt.decode(
result["access_token"],
"test-secret-padded-to-32-bytes!!",
algorithms=["HS256"],
options={"verify_aud": False},
)
assert "iss" not in payload
assert "aud" not in payload
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_iss_absent_when_none(mock_user):
"""iss should be absent from payload when issuer is None."""
service = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer=None,
audience=None,
expiration_seconds=3600,
token_type="Bearer",
)
result = service.generate_jwt(mock_user, scope="read")
payload = pyjwt.decode(
result["access_token"],
"test-secret-padded-to-32-bytes!!",
algorithms=["HS256"],
options={"verify_aud": False},
)
assert "iss" not in payload
assert "aud" not in payload
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_scope_absent_from_payload_when_empty(jwt_service, mock_user):
"""scope should be absent from payload when not provided."""
result = jwt_service.generate_jwt(mock_user, scope="")
payload = pyjwt.decode(
result["access_token"],
"test-secret-padded-to-32-bytes!!",
algorithms=["HS256"],
issuer="test-issuer",
audience="test-audience",
)
assert "scope" not in payload
# -- generate_jwt / extra_payload handling --
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_extra_payload_none_does_not_crash(jwt_service, mock_user):
"""extra_payload=None should not crash and produce a valid token."""
result = jwt_service.generate_jwt(mock_user, scope="read", extra_payload=None)
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["user_id"] == "test-user-id"
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_extra_payload_non_colliding_keys_preserved(
jwt_service, mock_user
):
"""Non-colliding extra_payload keys should appear in decoded token."""
result = jwt_service.generate_jwt(
mock_user,
scope="read",
extra_payload={"client_id": "my-app", "delegated": True},
)
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["client_id"] == "my-app"
assert payload["delegated"] is True
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_extra_payload_colliding_iat_overwritten(jwt_service, mock_user):
"""iat in extra_payload should be overwritten by the service."""
result = jwt_service.generate_jwt(mock_user, scope="read", extra_payload={"iat": 0})
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["iat"] == 1673784000
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_extra_payload_colliding_exp_overwritten(jwt_service, mock_user):
"""exp in extra_payload should be overwritten by the service."""
result = jwt_service.generate_jwt(
mock_user, scope="read", extra_payload={"exp": 9999999999}
)
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["exp"] == 1673787600
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_extra_payload_colliding_user_id_overwritten(
jwt_service, mock_user
):
"""user_id in extra_payload should be overwritten by the service."""
result = jwt_service.generate_jwt(
mock_user, scope="read", extra_payload={"user_id": "hacked"}
)
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["user_id"] == "test-user-id"
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_extra_payload_not_mutated(jwt_service, mock_user):
"""generate_jwt should not mutate the original extra_payload dict."""
extra = {"client_id": "my-app"}
jwt_service.generate_jwt(mock_user, scope="read", extra_payload=extra)
assert extra == {"client_id": "my-app"}
# -- generate_jwt / user.id casting --
@freeze_time("2023-01-15 12:00:00")
def test_generate_jwt_user_id_cast_from_uuid(jwt_service):
"""user.id as UUID should be cast to str in payload."""
user = mock.Mock()
user.id = uuid.UUID("12345678-1234-5678-1234-567812345678")
result = jwt_service.generate_jwt(user, scope="read")
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["user_id"] == "12345678-1234-5678-1234-567812345678"
# -- decode_jwt / Happy path --
def test_decode_jwt_roundtrip(jwt_service, mock_user):
"""Valid token should decode to correct payload."""
with freeze_time("2023-01-15 12:00:00"):
result = jwt_service.generate_jwt(
mock_user, scope="read", extra_payload={"client_id": "my-app"}
)
with freeze_time("2023-01-15 12:30:00"):
payload = jwt_service.decode_jwt(result["access_token"])
assert payload["user_id"] == "test-user-id"
assert payload["scope"] == "read"
assert payload["client_id"] == "my-app"
assert payload["iss"] == "test-issuer"
assert payload["aud"] == "test-audience"
# -- decode_jwt / Error mapping --
def test_decode_jwt_expired_raises_token_expired_error(jwt_service, mock_user):
"""Expired token should raise TokenExpiredError."""
with freeze_time("2023-01-15 12:00:00"):
result = jwt_service.generate_jwt(mock_user, scope="read")
with freeze_time("2099-01-01 00:00:00"):
with pytest.raises(TokenExpiredError):
jwt_service.decode_jwt(result["access_token"])
def test_decode_jwt_wrong_issuer_raises_token_invalid_error(mock_user):
"""Token with wrong issuer should raise TokenInvalidError."""
service_a = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer-a",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
service_b = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer-b",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
result = service_a.generate_jwt(mock_user, scope="read")
with pytest.raises(TokenInvalidError):
service_b.decode_jwt(result["access_token"])
def test_decode_jwt_wrong_audience_raises_token_invalid_error(mock_user):
"""Token with wrong audience should raise TokenInvalidError."""
service_a = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience-a",
expiration_seconds=3600,
token_type="Bearer",
)
service_b = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience-b",
expiration_seconds=3600,
token_type="Bearer",
)
result = service_a.generate_jwt(mock_user, scope="read")
with pytest.raises(TokenInvalidError):
service_b.decode_jwt(result["access_token"])
def test_decode_jwt_tampered_signature_raises_token_decode_error(
jwt_service, mock_user
):
"""Token with tampered signature should raise TokenDecodeError."""
result = jwt_service.generate_jwt(mock_user, scope="read")
header, payload, _ = result["access_token"].split(".")
tampered_token = f"{header}.{payload}.invalidsignature"
with pytest.raises(TokenDecodeError):
jwt_service.decode_jwt(tampered_token)
def test_decode_jwt_garbage_string_raises_token_decode_error(jwt_service):
"""Garbage string should raise TokenDecodeError."""
with pytest.raises(TokenDecodeError):
jwt_service.decode_jwt("this.is.not.a.valid.token")
def test_decode_jwt_empty_string_raises_token_decode_error(jwt_service):
"""Empty string should raise TokenDecodeError."""
with pytest.raises(TokenDecodeError):
jwt_service.decode_jwt("")
def test_decode_jwt_none_raises_token_decode_error(jwt_service):
"""None should raise TokenDecodeError."""
with pytest.raises(TokenDecodeError):
jwt_service.decode_jwt(None)
def test_algorithm_mismatch_raises_token_decode_error(mock_user):
"""Token encoded with HS256 decoded expecting RS256 should raise TokenDecodeError."""
service_hs256 = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="HS256",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
service_rs256 = JwtTokenService(
secret_key="test-secret-padded-to-32-bytes!!",
algorithm="RS256",
issuer="issuer",
audience="audience",
expiration_seconds=3600,
token_type="Bearer",
)
result = service_hs256.generate_jwt(mock_user, scope="read")
with pytest.raises(TokenDecodeError):
service_rs256.decode_jwt(result["access_token"])
@@ -713,7 +713,7 @@ def test_api_rooms_token_invalid_signature(mock_rs_authenticate, settings):
} }
token = jwt.encode( token = jwt.encode(
payload, payload,
"invalid-private-key-padded-to-32b!", "invalid-private-key",
algorithm=settings.APPLICATION_JWT_ALG, algorithm=settings.APPLICATION_JWT_ALG,
) )
+2 -12
View File
@@ -452,11 +452,7 @@ class Base(Configuration):
CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}, environ_prefix=None) CELERY_BROKER_TRANSPORT_OPTIONS = values.DictValue({}, environ_prefix=None)
# Session # Session
SESSION_ENGINE = values.Value( SESSION_ENGINE = "django.contrib.sessions.backends.cache"
default="django.contrib.sessions.backends.cache",
environ_name="SESSION_ENGINE",
environ_prefix=None,
)
SESSION_CACHE_ALIAS = "default" SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = values.PositiveIntegerValue( SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
@@ -981,13 +977,7 @@ class Test(Base):
USE_SWAGGER = True USE_SWAGGER = True
EXTERNAL_API_ENABLED = True EXTERNAL_API_ENABLED = True
LIVEKIT_CONFIGURATION = { APPLICATION_JWT_SECRET_KEY = "devKey" # noqa:S105
"api_key": "devkey-padded-for-minimum-len!-livekit",
"api_secret": "secret-key-padded-for-minimum-len!-livekit",
"url": "http://127.0.0.1.nip.io:7880",
}
APPLICATION_JWT_SECRET_KEY = "secret-key-padded-for-minimum-len!-application" # noqa:S105
APPLICATION_JWT_AUDIENCE = "Test inc." APPLICATION_JWT_AUDIENCE = "Test inc."
CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_ALWAYS_EAGER = True
+17 -22
View File
@@ -7,7 +7,7 @@ build-backend = "uv_build"
[project] [project]
name = "meet" name = "meet"
version = "1.13.0" version = "1.10.0"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }] authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [ classifiers = [
"Development Status :: 5 - Production/Stable", "Development Status :: 5 - Production/Stable",
@@ -24,16 +24,16 @@ keywords = ["Django", "Contacts", "Templates", "RBAC"]
license = "MIT" license = "MIT"
requires-python = ">=3.13" requires-python = ">=3.13"
dependencies = [ dependencies = [
"boto3==1.42.68", "boto3==1.42.49",
"Brotli==1.2.0", "Brotli==1.2.0",
"brevo-python==1.2.0", "brevo-python==1.2.0",
"celery[redis]==5.6.2", "celery[redis]==5.6.2",
"dj-database-url==3.1.2", "dj-database-url==3.1.0",
"django-configurations==2.5.1", "django-configurations==2.5.1",
"django-cors-headers==4.9.0", "django-cors-headers==4.9.0",
"django-countries==8.2.0", "django-countries==8.2.0",
"django-filter==25.2", "django-filter==25.2",
"django-lasuite[all]==0.0.25", "django-lasuite[all]==0.0.24",
"django-parler==2.3", "django-parler==2.3",
"redis==5.2.1", "redis==5.2.1",
"django-redis==6.0.0", "django-redis==6.0.0",
@@ -43,24 +43,24 @@ dependencies = [
"django==5.2.12", "django==5.2.12",
"djangorestframework==3.16.1", "djangorestframework==3.16.1",
"drf_spectacular==0.29.0", "drf_spectacular==0.29.0",
"dockerflow==2026.3.4", "dockerflow==2026.1.26",
"easy_thumbnails==2.10.1", "easy_thumbnails==2.10.1",
"factory_boy==3.3.3", "factory_boy==3.3.3",
"gunicorn==25.1.0", "gunicorn==25.1.0",
"jsonschema==4.26.0", "jsonschema==4.26.0",
"markdown==3.10.2", "markdown==3.10.2",
"nested-multipart-parser==1.6.0", "nested-multipart-parser==1.6.0",
"psycopg[binary]==3.3.3", "psycopg[binary]==3.3.2",
"pydantic==2.12.5", "pydantic==2.12.4",
"PyJWT==2.12.1", "PyJWT==2.11.0",
"python-frontmatter==1.1.0", "python-frontmatter==1.1.0",
"python-magic==0.4.27", "python-magic==0.4.27",
"requests==2.33.0", "requests==2.32.5",
"sentry-sdk==2.54.0", "sentry-sdk==2.53.0",
"whitenoise==6.12.0", "whitenoise==6.11.0",
"mozilla-django-oidc==5.0.2", "mozilla-django-oidc==5.0.2",
"livekit-api==1.1.0", "livekit-api==1.1.0",
"aiohttp==3.13.4", "aiohttp==3.13.3",
] ]
[project.urls] [project.urls]
@@ -72,11 +72,11 @@ dependencies = [
[dependency-groups] [dependency-groups]
dev = [ dev = [
"django-extensions==4.1", "django-extensions==4.1",
"drf-spectacular-sidecar==2026.3.1", "drf-spectacular-sidecar==2026.1.1",
"freezegun==1.5.5", "freezegun==1.5.5",
"ipdb==0.13.13", "ipdb==0.13.13",
"ipython==9.11.0", "ipython==9.10.0",
"pyfakefs==6.1.5", "pyfakefs==6.1.1",
"pylint-django==2.7.0", "pylint-django==2.7.0",
"pylint<4.0.0", "pylint<4.0.0",
"pytest-cov==7.0.0", "pytest-cov==7.0.0",
@@ -84,17 +84,12 @@ dev = [
"pytest==9.0.2", "pytest==9.0.2",
"pytest-icdiff==0.9", "pytest-icdiff==0.9",
"pytest-xdist==3.8.0", "pytest-xdist==3.8.0",
"responses==0.26.0", "responses==0.25.8",
"ruff==0.15.6", "ruff==0.15.1",
"types-requests==2.32.4.20260107", "types-requests==2.32.4.20260107",
] ]
[tool.uv.build-backend] [tool.uv.build-backend]
module-name = [
"core",
"demo",
"meet"
]
module-root = "" module-root = ""
source-exclude = [ source-exclude = [
"**/tests/**", "**/tests/**",
+201 -201
View File
@@ -13,7 +13,7 @@ wheels = [
[[package]] [[package]]
name = "aiohttp" name = "aiohttp"
version = "3.13.4" version = "3.13.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "aiohappyeyeballs" }, { name = "aiohappyeyeballs" },
@@ -24,59 +24,59 @@ dependencies = [
{ name = "propcache" }, { name = "propcache" },
{ name = "yarl" }, { name = "yarl" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/45/4a/064321452809dae953c1ed6e017504e72551a26b6f5708a5a80e4bf556ff/aiohttp-3.13.4.tar.gz", hash = "sha256:d97a6d09c66087890c2ab5d49069e1e570583f7ac0314ecf98294c1b6aaebd38", size = 7859748, upload-time = "2026-03-28T17:19:40.6Z" } sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e3/ac/892f4162df9b115b4758d615f32ec63d00f3084c705ff5526630887b9b42/aiohttp-3.13.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:63dd5e5b1e43b8fb1e91b79b7ceba1feba588b317d1edff385084fcc7a0a4538", size = 745744, upload-time = "2026-03-28T17:16:44.67Z" }, { url = "https://files.pythonhosted.org/packages/97/8a/12ca489246ca1faaf5432844adbfce7ff2cc4997733e0af120869345643a/aiohttp-3.13.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c", size = 734190, upload-time = "2026-01-03T17:30:45.832Z" },
{ url = "https://files.pythonhosted.org/packages/97/a9/c5b87e4443a2f0ea88cb3000c93a8fdad1ee63bffc9ded8d8c8e0d66efc6/aiohttp-3.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:746ac3cc00b5baea424dacddea3ec2c2702f9590de27d837aa67004db1eebc6e", size = 498178, upload-time = "2026-03-28T17:16:46.766Z" }, { url = "https://files.pythonhosted.org/packages/32/08/de43984c74ed1fca5c014808963cc83cb00d7bb06af228f132d33862ca76/aiohttp-3.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9", size = 491783, upload-time = "2026-01-03T17:30:47.466Z" },
{ url = "https://files.pythonhosted.org/packages/94/42/07e1b543a61250783650df13da8ddcdc0d0a5538b2bd15cef6e042aefc61/aiohttp-3.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bda8f16ea99d6a6705e5946732e48487a448be874e54a4f73d514660ff7c05d3", size = 498331, upload-time = "2026-03-28T17:16:48.9Z" }, { url = "https://files.pythonhosted.org/packages/17/f8/8dd2cf6112a5a76f81f81a5130c57ca829d101ad583ce57f889179accdda/aiohttp-3.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3", size = 490704, upload-time = "2026-01-03T17:30:49.373Z" },
{ url = "https://files.pythonhosted.org/packages/20/d6/492f46bf0328534124772d0cf58570acae5b286ea25006900650f69dae0e/aiohttp-3.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b061e7b5f840391e3f64d0ddf672973e45c4cfff7a0feea425ea24e51530fc2", size = 1744414, upload-time = "2026-03-28T17:16:50.968Z" }, { url = "https://files.pythonhosted.org/packages/6d/40/a46b03ca03936f832bc7eaa47cfbb1ad012ba1be4790122ee4f4f8cba074/aiohttp-3.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf", size = 1720652, upload-time = "2026-01-03T17:30:50.974Z" },
{ url = "https://files.pythonhosted.org/packages/e2/4d/e02627b2683f68051246215d2d62b2d2f249ff7a285e7a858dc47d6b6a14/aiohttp-3.13.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b252e8d5cd66184b570d0d010de742736e8a4fab22c58299772b0c5a466d4b21", size = 1719226, upload-time = "2026-03-28T17:16:53.173Z" }, { url = "https://files.pythonhosted.org/packages/f7/7e/917fe18e3607af92657e4285498f500dca797ff8c918bd7d90b05abf6c2a/aiohttp-3.13.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6", size = 1692014, upload-time = "2026-01-03T17:30:52.729Z" },
{ url = "https://files.pythonhosted.org/packages/7b/6c/5d0a3394dd2b9f9aeba6e1b6065d0439e4b75d41f1fb09a3ec010b43552b/aiohttp-3.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20af8aad61d1803ff11152a26146d8d81c266aa8c5aa9b4504432abb965c36a0", size = 1782110, upload-time = "2026-03-28T17:16:55.362Z" }, { url = "https://files.pythonhosted.org/packages/71/b6/cefa4cbc00d315d68973b671cf105b21a609c12b82d52e5d0c9ae61d2a09/aiohttp-3.13.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d", size = 1759777, upload-time = "2026-01-03T17:30:54.537Z" },
{ url = "https://files.pythonhosted.org/packages/0d/2d/c20791e3437700a7441a7edfb59731150322424f5aadf635602d1d326101/aiohttp-3.13.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:13a5cc924b59859ad2adb1478e31f410a7ed46e92a2a619d6d1dd1a63c1a855e", size = 1884809, upload-time = "2026-03-28T17:16:57.734Z" }, { url = "https://files.pythonhosted.org/packages/fb/e3/e06ee07b45e59e6d81498b591fc589629be1553abb2a82ce33efe2a7b068/aiohttp-3.13.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261", size = 1861276, upload-time = "2026-01-03T17:30:56.512Z" },
{ url = "https://files.pythonhosted.org/packages/c8/94/d99dbfbd1924a87ef643833932eb2a3d9e5eee87656efea7d78058539eff/aiohttp-3.13.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:534913dfb0a644d537aebb4123e7d466d94e3be5549205e6a31f72368980a81a", size = 1764938, upload-time = "2026-03-28T17:17:00.221Z" }, { url = "https://files.pythonhosted.org/packages/7c/24/75d274228acf35ceeb2850b8ce04de9dd7355ff7a0b49d607ee60c29c518/aiohttp-3.13.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0", size = 1743131, upload-time = "2026-01-03T17:30:58.256Z" },
{ url = "https://files.pythonhosted.org/packages/49/61/3ce326a1538781deb89f6cf5e094e2029cd308ed1e21b2ba2278b08426f6/aiohttp-3.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:320e40192a2dcc1cf4b5576936e9652981ab596bf81eb309535db7e2f5b5672f", size = 1570697, upload-time = "2026-03-28T17:17:02.985Z" }, { url = "https://files.pythonhosted.org/packages/04/98/3d21dde21889b17ca2eea54fdcff21b27b93f45b7bb94ca029c31ab59dc3/aiohttp-3.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730", size = 1556863, upload-time = "2026-01-03T17:31:00.445Z" },
{ url = "https://files.pythonhosted.org/packages/b6/77/4ab5a546857bb3028fbaf34d6eea180267bdab022ee8b1168b1fcde4bfdd/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9e587fcfce2bcf06526a43cb705bdee21ac089096f2e271d75de9c339db3100c", size = 1702258, upload-time = "2026-03-28T17:17:05.28Z" }, { url = "https://files.pythonhosted.org/packages/9e/84/da0c3ab1192eaf64782b03971ab4055b475d0db07b17eff925e8c93b3aa5/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91", size = 1682793, upload-time = "2026-01-03T17:31:03.024Z" },
{ url = "https://files.pythonhosted.org/packages/79/63/d8f29021e39bc5af8e5d5e9da1b07976fb9846487a784e11e4f4eeda4666/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9eb9c2eea7278206b5c6c1441fdd9dc420c278ead3f3b2cc87f9b693698cc500", size = 1740287, upload-time = "2026-03-28T17:17:07.712Z" }, { url = "https://files.pythonhosted.org/packages/ff/0f/5802ada182f575afa02cbd0ec5180d7e13a402afb7c2c03a9aa5e5d49060/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3", size = 1716676, upload-time = "2026-01-03T17:31:04.842Z" },
{ url = "https://files.pythonhosted.org/packages/55/3a/cbc6b3b124859a11bc8055d3682c26999b393531ef926754a3445b99dfef/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:29be00c51972b04bf9d5c8f2d7f7314f48f96070ca40a873a53056e652e805f7", size = 1753011, upload-time = "2026-03-28T17:17:10.053Z" }, { url = "https://files.pythonhosted.org/packages/3f/8c/714d53bd8b5a4560667f7bbbb06b20c2382f9c7847d198370ec6526af39c/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4", size = 1733217, upload-time = "2026-01-03T17:31:06.868Z" },
{ url = "https://files.pythonhosted.org/packages/e0/30/836278675205d58c1368b21520eab9572457cf19afd23759216c04483048/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90c06228a6c3a7c9f776fe4fc0b7ff647fffd3bed93779a6913c804ae00c1073", size = 1566359, upload-time = "2026-03-28T17:17:12.433Z" }, { url = "https://files.pythonhosted.org/packages/7d/79/e2176f46d2e963facea939f5be2d26368ce543622be6f00a12844d3c991f/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998", size = 1552303, upload-time = "2026-01-03T17:31:08.958Z" },
{ url = "https://files.pythonhosted.org/packages/50/b4/8032cc9b82d17e4277704ba30509eaccb39329dc18d6a35f05e424439e32/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a533ec132f05fd9a1d959e7f34184cd7d5e8511584848dab85faefbaac573069", size = 1785537, upload-time = "2026-03-28T17:17:14.721Z" }, { url = "https://files.pythonhosted.org/packages/ab/6a/28ed4dea1759916090587d1fe57087b03e6c784a642b85ef48217b0277ae/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0", size = 1763673, upload-time = "2026-01-03T17:31:10.676Z" },
{ url = "https://files.pythonhosted.org/packages/17/7d/5873e98230bde59f493bf1f7c3e327486a4b5653fa401144704df5d00211/aiohttp-3.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1c946f10f413836f82ea4cfb90200d2a59578c549f00857e03111cf45ad01ca5", size = 1740752, upload-time = "2026-03-28T17:17:17.387Z" }, { url = "https://files.pythonhosted.org/packages/e8/35/4a3daeb8b9fab49240d21c04d50732313295e4bd813a465d840236dd0ce1/aiohttp-3.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591", size = 1721120, upload-time = "2026-01-03T17:31:12.575Z" },
{ url = "https://files.pythonhosted.org/packages/7b/f2/13e46e0df051494d7d3c68b7f72d071f48c384c12716fc294f75d5b1a064/aiohttp-3.13.4-cp313-cp313-win32.whl", hash = "sha256:48708e2706106da6967eff5908c78ca3943f005ed6bcb75da2a7e4da94ef8c70", size = 433187, upload-time = "2026-03-28T17:17:19.523Z" }, { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" },
{ url = "https://files.pythonhosted.org/packages/ea/c0/649856ee655a843c8f8664592cfccb73ac80ede6a8c8db33a25d810c12db/aiohttp-3.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:74a2eb058da44fa3a877a49e2095b591d4913308bb424c418b77beb160c55ce3", size = 459778, upload-time = "2026-03-28T17:17:21.964Z" }, { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
{ url = "https://files.pythonhosted.org/packages/6d/29/6657cc37ae04cacc2dbf53fb730a06b6091cc4cbe745028e047c53e6d840/aiohttp-3.13.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:e0a2c961fc92abeff61d6444f2ce6ad35bb982db9fc8ff8a47455beacf454a57", size = 749363, upload-time = "2026-03-28T17:17:24.044Z" }, { url = "https://files.pythonhosted.org/packages/99/36/5b6514a9f5d66f4e2597e40dea2e3db271e023eb7a5d22defe96ba560996/aiohttp-3.13.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808", size = 737238, upload-time = "2026-01-03T17:31:17.909Z" },
{ url = "https://files.pythonhosted.org/packages/90/7f/30ccdf67ca3d24b610067dc63d64dcb91e5d88e27667811640644aa4a85d/aiohttp-3.13.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:153274535985a0ff2bff1fb6c104ed547cec898a09213d21b0f791a44b14d933", size = 499317, upload-time = "2026-03-28T17:17:26.199Z" }, { url = "https://files.pythonhosted.org/packages/f7/49/459327f0d5bcd8c6c9ca69e60fdeebc3622861e696490d8674a6d0cb90a6/aiohttp-3.13.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415", size = 492292, upload-time = "2026-01-03T17:31:19.919Z" },
{ url = "https://files.pythonhosted.org/packages/93/13/e372dd4e68ad04ee25dafb050c7f98b0d91ea643f7352757e87231102555/aiohttp-3.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:351f3171e2458da3d731ce83f9e6b9619e325c45cbd534c7759750cabf453ad7", size = 500477, upload-time = "2026-03-28T17:17:28.279Z" }, { url = "https://files.pythonhosted.org/packages/e8/0b/b97660c5fd05d3495b4eb27f2d0ef18dc1dc4eff7511a9bf371397ff0264/aiohttp-3.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f", size = 493021, upload-time = "2026-01-03T17:31:21.636Z" },
{ url = "https://files.pythonhosted.org/packages/e5/fe/ee6298e8e586096fb6f5eddd31393d8544f33ae0792c71ecbb4c2bef98ac/aiohttp-3.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f989ac8bc5595ff761a5ccd32bdb0768a117f36dd1504b1c2c074ed5d3f4df9c", size = 1737227, upload-time = "2026-03-28T17:17:30.587Z" }, { url = "https://files.pythonhosted.org/packages/54/d4/438efabdf74e30aeceb890c3290bbaa449780583b1270b00661126b8aae4/aiohttp-3.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6", size = 1717263, upload-time = "2026-01-03T17:31:23.296Z" },
{ url = "https://files.pythonhosted.org/packages/b0/b9/a7a0463a09e1a3fe35100f74324f23644bfc3383ac5fd5effe0722a5f0b7/aiohttp-3.13.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d36fc1709110ec1e87a229b201dd3ddc32aa01e98e7868083a794609b081c349", size = 1694036, upload-time = "2026-03-28T17:17:33.29Z" }, { url = "https://files.pythonhosted.org/packages/71/f2/7bddc7fd612367d1459c5bcf598a9e8f7092d6580d98de0e057eb42697ad/aiohttp-3.13.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687", size = 1669107, upload-time = "2026-01-03T17:31:25.334Z" },
{ url = "https://files.pythonhosted.org/packages/57/7c/8972ae3fb7be00a91aee6b644b2a6a909aedb2c425269a3bfd90115e6f8f/aiohttp-3.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:42adaeea83cbdf069ab94f5103ce0787c21fb1a0153270da76b59d5578302329", size = 1786814, upload-time = "2026-03-28T17:17:36.035Z" }, { url = "https://files.pythonhosted.org/packages/00/5a/1aeaecca40e22560f97610a329e0e5efef5e0b5afdf9f857f0d93839ab2e/aiohttp-3.13.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26", size = 1760196, upload-time = "2026-01-03T17:31:27.394Z" },
{ url = "https://files.pythonhosted.org/packages/93/01/c81e97e85c774decbaf0d577de7d848934e8166a3a14ad9f8aa5be329d28/aiohttp-3.13.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:92deb95469928cc41fd4b42a95d8012fa6df93f6b1c0a83af0ffbc4a5e218cde", size = 1866676, upload-time = "2026-03-28T17:17:38.441Z" }, { url = "https://files.pythonhosted.org/packages/f8/f8/0ff6992bea7bd560fc510ea1c815f87eedd745fe035589c71ce05612a19a/aiohttp-3.13.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a", size = 1843591, upload-time = "2026-01-03T17:31:29.238Z" },
{ url = "https://files.pythonhosted.org/packages/5a/5f/5b46fe8694a639ddea2cd035bf5729e4677ea882cb251396637e2ef1590d/aiohttp-3.13.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0c0c7c07c4257ef3a1df355f840bc62d133bcdef5c1c5ba75add3c08553e2eed", size = 1740842, upload-time = "2026-03-28T17:17:40.783Z" }, { url = "https://files.pythonhosted.org/packages/e3/d1/e30e537a15f53485b61f5be525f2157da719819e8377298502aebac45536/aiohttp-3.13.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1", size = 1720277, upload-time = "2026-01-03T17:31:31.053Z" },
{ url = "https://files.pythonhosted.org/packages/20/a2/0d4b03d011cca6b6b0acba8433193c1e484efa8d705ea58295590fe24203/aiohttp-3.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f062c45de8a1098cb137a1898819796a2491aec4e637a06b03f149315dff4d8f", size = 1566508, upload-time = "2026-03-28T17:17:43.235Z" }, { url = "https://files.pythonhosted.org/packages/84/45/23f4c451d8192f553d38d838831ebbc156907ea6e05557f39563101b7717/aiohttp-3.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25", size = 1548575, upload-time = "2026-01-03T17:31:32.87Z" },
{ url = "https://files.pythonhosted.org/packages/98/17/e689fd500da52488ec5f889effd6404dece6a59de301e380f3c64f167beb/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:76093107c531517001114f0ebdb4f46858ce818590363e3e99a4a2280334454a", size = 1700569, upload-time = "2026-03-28T17:17:46.165Z" }, { url = "https://files.pythonhosted.org/packages/6a/ed/0a42b127a43712eda7807e7892c083eadfaf8429ca8fb619662a530a3aab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603", size = 1679455, upload-time = "2026-01-03T17:31:34.76Z" },
{ url = "https://files.pythonhosted.org/packages/d8/0d/66402894dbcf470ef7db99449e436105ea862c24f7ea4c95c683e635af35/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6f6ec32162d293b82f8b63a16edc80769662fbd5ae6fbd4936d3206a2c2cc63b", size = 1707407, upload-time = "2026-03-28T17:17:48.825Z" }, { url = "https://files.pythonhosted.org/packages/2e/b5/c05f0c2b4b4fe2c9d55e73b6d3ed4fd6c9dc2684b1d81cbdf77e7fad9adb/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a", size = 1687417, upload-time = "2026-01-03T17:31:36.699Z" },
{ url = "https://files.pythonhosted.org/packages/2f/eb/af0ab1a3650092cbd8e14ef29e4ab0209e1460e1c299996c3f8288b3f1ff/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5903e2db3d202a00ad9f0ec35a122c005e85d90c9836ab4cda628f01edf425e2", size = 1752214, upload-time = "2026-03-28T17:17:51.206Z" }, { url = "https://files.pythonhosted.org/packages/c9/6b/915bc5dad66aef602b9e459b5a973529304d4e89ca86999d9d75d80cbd0b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926", size = 1729968, upload-time = "2026-01-03T17:31:38.622Z" },
{ url = "https://files.pythonhosted.org/packages/5a/bf/72326f8a98e4c666f292f03c385545963cc65e358835d2a7375037a97b57/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2d5bea57be7aca98dbbac8da046d99b5557c5cf4e28538c4c786313078aca09e", size = 1562162, upload-time = "2026-03-28T17:17:53.634Z" }, { url = "https://files.pythonhosted.org/packages/11/3b/e84581290a9520024a08640b63d07673057aec5ca548177a82026187ba73/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba", size = 1545690, upload-time = "2026-01-03T17:31:40.57Z" },
{ url = "https://files.pythonhosted.org/packages/67/9f/13b72435f99151dd9a5469c96b3b5f86aa29b7e785ca7f35cf5e538f74c0/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bcf0c9902085976edc0232b75006ef38f89686901249ce14226b6877f88464fb", size = 1768904, upload-time = "2026-03-28T17:17:55.991Z" }, { url = "https://files.pythonhosted.org/packages/f5/04/0c3655a566c43fd647c81b895dfe361b9f9ad6d58c19309d45cff52d6c3b/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c", size = 1746390, upload-time = "2026-01-03T17:31:42.857Z" },
{ url = "https://files.pythonhosted.org/packages/18/bc/28d4970e7d5452ac7776cdb5431a1164a0d9cf8bd2fffd67b4fb463aa56d/aiohttp-3.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3295f98bfeed2e867cab588f2a146a9db37a85e3ae9062abf46ba062bd29165", size = 1723378, upload-time = "2026-03-28T17:17:58.348Z" }, { url = "https://files.pythonhosted.org/packages/1f/53/71165b26978f719c3419381514c9690bd5980e764a09440a10bb816ea4ab/aiohttp-3.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43", size = 1702188, upload-time = "2026-01-03T17:31:44.984Z" },
{ url = "https://files.pythonhosted.org/packages/53/74/b32458ca1a7f34d65bdee7aef2036adbe0438123d3d53e2b083c453c24dd/aiohttp-3.13.4-cp314-cp314-win32.whl", hash = "sha256:a598a5c5767e1369d8f5b08695cab1d8160040f796c4416af76fd773d229b3c9", size = 438711, upload-time = "2026-03-28T17:18:00.728Z" }, { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" },
{ url = "https://files.pythonhosted.org/packages/40/b2/54b487316c2df3e03a8f3435e9636f8a81a42a69d942164830d193beb56a/aiohttp-3.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:c555db4bc7a264bead5a7d63d92d41a1122fcd39cc62a4db815f45ad46f9c2c8", size = 464977, upload-time = "2026-03-28T17:18:03.367Z" }, { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" },
{ url = "https://files.pythonhosted.org/packages/47/fb/e41b63c6ce71b07a59243bb8f3b457ee0c3402a619acb9d2c0d21ef0e647/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:45abbbf09a129825d13c18c7d3182fecd46d9da3cfc383756145394013604ac1", size = 781549, upload-time = "2026-03-28T17:18:05.779Z" }, { url = "https://files.pythonhosted.org/packages/6c/2a/3c79b638a9c3d4658d345339d22070241ea341ed4e07b5ac60fb0f418003/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c", size = 769512, upload-time = "2026-01-03T17:31:51.134Z" },
{ url = "https://files.pythonhosted.org/packages/97/53/532b8d28df1e17e44c4d9a9368b78dcb6bf0b51037522136eced13afa9e8/aiohttp-3.13.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:74c80b2bc2c2adb7b3d1941b2b60701ee2af8296fc8aad8b8bc48bc25767266c", size = 514383, upload-time = "2026-03-28T17:18:08.096Z" }, { url = "https://files.pythonhosted.org/packages/29/b9/3e5014d46c0ab0db8707e0ac2711ed28c4da0218c358a4e7c17bae0d8722/aiohttp-3.13.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592", size = 506444, upload-time = "2026-01-03T17:31:52.85Z" },
{ url = "https://files.pythonhosted.org/packages/1b/1f/62e5d400603e8468cd635812d99cb81cfdc08127a3dc474c647615f31339/aiohttp-3.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c97989ae40a9746650fa196894f317dafc12227c808c774929dda0ff873a5954", size = 518304, upload-time = "2026-03-28T17:18:10.642Z" }, { url = "https://files.pythonhosted.org/packages/90/03/c1d4ef9a054e151cd7839cdc497f2638f00b93cbe8043983986630d7a80c/aiohttp-3.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f", size = 510798, upload-time = "2026-01-03T17:31:54.91Z" },
{ url = "https://files.pythonhosted.org/packages/90/57/2326b37b10896447e3c6e0cbef4fe2486d30913639a5cfd1332b5d870f82/aiohttp-3.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dae86be9811493f9990ef44fff1685f5c1a3192e9061a71a109d527944eed551", size = 1893433, upload-time = "2026-03-28T17:18:13.121Z" }, { url = "https://files.pythonhosted.org/packages/ea/76/8c1e5abbfe8e127c893fe7ead569148a4d5a799f7cf958d8c09f3eedf097/aiohttp-3.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29", size = 1868835, upload-time = "2026-01-03T17:31:56.733Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b4/a24d82112c304afdb650167ef2fe190957d81cbddac7460bedd245f765aa/aiohttp-3.13.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1db491abe852ca2fa6cc48a3341985b0174b3741838e1341b82ac82c8bd9e871", size = 1755901, upload-time = "2026-03-28T17:18:16.21Z" }, { url = "https://files.pythonhosted.org/packages/8e/ac/984c5a6f74c363b01ff97adc96a3976d9c98940b8969a1881575b279ac5d/aiohttp-3.13.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc", size = 1720486, upload-time = "2026-01-03T17:31:58.65Z" },
{ url = "https://files.pythonhosted.org/packages/9e/2d/0883ef9d878d7846287f036c162a951968f22aabeef3ac97b0bea6f76d5d/aiohttp-3.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e5d701c0aad02a7dce72eef6b93226cf3734330f1a31d69ebbf69f33b86666e", size = 1876093, upload-time = "2026-03-28T17:18:18.703Z" }, { url = "https://files.pythonhosted.org/packages/b2/9a/b7039c5f099c4eb632138728828b33428585031a1e658d693d41d07d89d1/aiohttp-3.13.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2", size = 1847951, upload-time = "2026-01-03T17:32:00.989Z" },
{ url = "https://files.pythonhosted.org/packages/ad/52/9204bb59c014869b71971addad6778f005daa72a96eed652c496789d7468/aiohttp-3.13.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ac32a189081ae0a10ba18993f10f338ec94341f0d5df8fff348043962f3c6f8", size = 1970815, upload-time = "2026-03-28T17:18:21.858Z" }, { url = "https://files.pythonhosted.org/packages/3c/02/3bec2b9a1ba3c19ff89a43a19324202b8eb187ca1e928d8bdac9bbdddebd/aiohttp-3.13.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587", size = 1941001, upload-time = "2026-01-03T17:32:03.122Z" },
{ url = "https://files.pythonhosted.org/packages/d6/b5/e4eb20275a866dde0f570f411b36c6b48f7b53edfe4f4071aa1b0728098a/aiohttp-3.13.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98e968cdaba43e45c73c3f306fca418c8009a957733bac85937c9f9cf3f4de27", size = 1816223, upload-time = "2026-03-28T17:18:24.729Z" }, { url = "https://files.pythonhosted.org/packages/37/df/d879401cedeef27ac4717f6426c8c36c3091c6e9f08a9178cc87549c537f/aiohttp-3.13.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8", size = 1797246, upload-time = "2026-01-03T17:32:05.255Z" },
{ url = "https://files.pythonhosted.org/packages/d8/23/e98075c5bb146aa61a1239ee1ac7714c85e814838d6cebbe37d3fe19214a/aiohttp-3.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca114790c9144c335d538852612d3e43ea0f075288f4849cf4b05d6cd2238ce7", size = 1649145, upload-time = "2026-03-28T17:18:27.269Z" }, { url = "https://files.pythonhosted.org/packages/8d/15/be122de1f67e6953add23335c8ece6d314ab67c8bebb3f181063010795a7/aiohttp-3.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632", size = 1627131, upload-time = "2026-01-03T17:32:07.607Z" },
{ url = "https://files.pythonhosted.org/packages/d6/c1/7bad8be33bb06c2bb224b6468874346026092762cbec388c3bdb65a368ee/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ea2e071661ba9cfe11eabbc81ac5376eaeb3061f6e72ec4cc86d7cdd1ffbdbbb", size = 1816562, upload-time = "2026-03-28T17:18:29.847Z" }, { url = "https://files.pythonhosted.org/packages/12/12/70eedcac9134cfa3219ab7af31ea56bc877395b1ac30d65b1bc4b27d0438/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64", size = 1795196, upload-time = "2026-01-03T17:32:09.59Z" },
{ url = "https://files.pythonhosted.org/packages/5c/10/c00323348695e9a5e316825969c88463dcc24c7e9d443244b8a2c9cf2eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:34e89912b6c20e0fd80e07fa401fd218a410aa1ce9f1c2f1dad6db1bd0ce0927", size = 1800333, upload-time = "2026-03-28T17:18:32.269Z" }, { url = "https://files.pythonhosted.org/packages/32/11/b30e1b1cd1f3054af86ebe60df96989c6a414dd87e27ad16950eee420bea/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0", size = 1782841, upload-time = "2026-01-03T17:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/84/43/9b2147a1df3559f49bd723e22905b46a46c068a53adb54abdca32c4de180/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0e217cf9f6a42908c52b46e42c568bd57adc39c9286ced31aaace614b6087965", size = 1820617, upload-time = "2026-03-28T17:18:35.238Z" }, { url = "https://files.pythonhosted.org/packages/88/0d/d98a9367b38912384a17e287850f5695c528cff0f14f791ce8ee2e4f7796/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56", size = 1795193, upload-time = "2026-01-03T17:32:13.705Z" },
{ url = "https://files.pythonhosted.org/packages/a9/7f/b3481a81e7a586d02e99387b18c6dafff41285f6efd3daa2124c01f87eae/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:0c296f1221e21ba979f5ac1964c3b78cfde15c5c5f855ffd2caab337e9cd9182", size = 1643417, upload-time = "2026-03-28T17:18:37.949Z" }, { url = "https://files.pythonhosted.org/packages/43/a5/a2dfd1f5ff5581632c7f6a30e1744deda03808974f94f6534241ef60c751/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72", size = 1621979, upload-time = "2026-01-03T17:32:15.965Z" },
{ url = "https://files.pythonhosted.org/packages/8f/72/07181226bc99ce1124e0f89280f5221a82d3ae6a6d9d1973ce429d48e52b/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d99a9d168ebaffb74f36d011750e490085ac418f4db926cce3989c8fe6cb6b1b", size = 1849286, upload-time = "2026-03-28T17:18:40.534Z" }, { url = "https://files.pythonhosted.org/packages/fa/f0/12973c382ae7c1cccbc4417e129c5bf54c374dfb85af70893646e1f0e749/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df", size = 1822193, upload-time = "2026-01-03T17:32:18.219Z" },
{ url = "https://files.pythonhosted.org/packages/1a/e6/1b3566e103eca6da5be4ae6713e112a053725c584e96574caf117568ffef/aiohttp-3.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cb19177205d93b881f3f89e6081593676043a6828f59c78c17a0fd6c1fbed2ba", size = 1782635, upload-time = "2026-03-28T17:18:43.073Z" }, { url = "https://files.pythonhosted.org/packages/3c/5f/24155e30ba7f8c96918af1350eb0663e2430aad9e001c0489d89cd708ab1/aiohttp-3.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa", size = 1769801, upload-time = "2026-01-03T17:32:20.25Z" },
{ url = "https://files.pythonhosted.org/packages/37/58/1b11c71904b8d079eb0c39fe664180dd1e14bebe5608e235d8bfbadc8929/aiohttp-3.13.4-cp314-cp314t-win32.whl", hash = "sha256:c606aa5656dab6552e52ca368e43869c916338346bfaf6304e15c58fb113ea30", size = 472537, upload-time = "2026-03-28T17:18:46.286Z" }, { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" },
{ url = "https://files.pythonhosted.org/packages/bc/8f/87c56a1a1977d7dddea5b31e12189665a140fdb48a71e9038ff90bb564ec/aiohttp-3.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:014dcc10ec8ab8db681f0d68e939d1e9286a5aa2b993cbbdb0db130853e02144", size = 506381, upload-time = "2026-03-28T17:18:48.74Z" }, { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" },
] ]
[[package]] [[package]]
@@ -159,30 +159,30 @@ wheels = [
[[package]] [[package]]
name = "boto3" name = "boto3"
version = "1.42.68" version = "1.42.49"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "botocore" }, { name = "botocore" },
{ name = "jmespath" }, { name = "jmespath" },
{ name = "s3transfer" }, { name = "s3transfer" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/06/ae/60c642aa5413e560b671da825329f510b29a77274ed0f580bde77562294d/boto3-1.42.68.tar.gz", hash = "sha256:3f349f967ab38c23425626d130962bcb363e75f042734fe856ea8c5a00eef03c", size = 112761, upload-time = "2026-03-13T19:32:17.137Z" } sdist = { url = "https://files.pythonhosted.org/packages/20/91/105aa17e0f3a566d33e2d8a3b32a70f553b1ad500d9756c6dd63991d8354/boto3-1.42.49.tar.gz", hash = "sha256:9cd252f640567b86e92b0a8ffdd4ade9a3018ee357c724bff6a21b8c8a41be0c", size = 112877, upload-time = "2026-02-13T20:29:57.062Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/f6/dc6e993479dbb597d68223fbf61cb026511737696b15bd7d2a33e9b2c24f/boto3-1.42.68-py3-none-any.whl", hash = "sha256:dbff353eb7dc93cbddd7926ed24793e0174c04adbe88860dfa639568442e4962", size = 140556, upload-time = "2026-03-13T19:32:14.951Z" }, { url = "https://files.pythonhosted.org/packages/12/b1/1fa30cd7b26617d59efbe3a4f3660a5b8b397a4623bf1e67016c4cb6dd0e/boto3-1.42.49-py3-none-any.whl", hash = "sha256:99e1df4361c3f6ff6ade65803c043ea96314826134962dd3b385433b309eb819", size = 140606, upload-time = "2026-02-13T20:29:55.366Z" },
] ]
[[package]] [[package]]
name = "botocore" name = "botocore"
version = "1.42.73" version = "1.42.64"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "jmespath" }, { name = "jmespath" },
{ name = "python-dateutil" }, { name = "python-dateutil" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/28/23/0c88ca116ef63b1ae77c901cd5d2095d22a8dbde9e80df74545db4a061b4/botocore-1.42.73.tar.gz", hash = "sha256:575858641e4949aaf2af1ced145b8524529edf006d075877af6b82ff96ad854c", size = 15008008, upload-time = "2026-03-20T19:39:40.082Z" } sdist = { url = "https://files.pythonhosted.org/packages/d3/3c/ac4bc939da695d2c648bf28f7b204ab741e4504e81749ccf943403cc07ca/botocore-1.42.64.tar.gz", hash = "sha256:4ee2aece227b9171ace8b749af694a77ab984fceab1639f2626bd0d6fb1aa69d", size = 14967869, upload-time = "2026-03-09T19:51:46.213Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/65/971f3d55015f4d133a6ff3ad74cd39f4b8dd8f53f7775a3c2ad378ea5145/botocore-1.42.73-py3-none-any.whl", hash = "sha256:7b62e2a12f7a1b08eb7360eecd23bb16fe3b7ab7f5617cf91b25476c6f86a0fe", size = 14681861, upload-time = "2026-03-20T19:39:35.341Z" }, { url = "https://files.pythonhosted.org/packages/33/0f/a0feb9a93da8f583217432dce71ce1940d6d8aa5884bad340872a504ba3f/botocore-1.42.64-py3-none-any.whl", hash = "sha256:f77c5cb76ed30576ed0bc73b591265d03dddffff02a9208d3ee0c790f43d3cd2", size = 14641339, upload-time = "2026-03-09T19:51:41.244Z" },
] ]
[[package]] [[package]]
@@ -474,55 +474,55 @@ wheels = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.6" version = "46.0.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/a4/ba/04b1bd4218cbc58dc90ce967106d51582371b898690f3ae0402876cc4f34/cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759", size = 750542, upload-time = "2026-03-25T23:34:53.396Z" } sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/47/23/9285e15e3bc57325b0a72e592921983a701efc1ee8f91c06c5f0235d86d9/cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8", size = 7176401, upload-time = "2026-03-25T23:33:22.096Z" }, { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" },
{ url = "https://files.pythonhosted.org/packages/60/f8/e61f8f13950ab6195b31913b42d39f0f9afc7d93f76710f299b5ec286ae6/cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30", size = 4275275, upload-time = "2026-03-25T23:33:23.844Z" }, { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" },
{ url = "https://files.pythonhosted.org/packages/19/69/732a736d12c2631e140be2348b4ad3d226302df63ef64d30dfdb8db7ad1c/cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a", size = 4425320, upload-time = "2026-03-25T23:33:25.703Z" }, { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" },
{ url = "https://files.pythonhosted.org/packages/d4/12/123be7292674abf76b21ac1fc0e1af50661f0e5b8f0ec8285faac18eb99e/cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175", size = 4278082, upload-time = "2026-03-25T23:33:27.423Z" }, { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" },
{ url = "https://files.pythonhosted.org/packages/5b/ba/d5e27f8d68c24951b0a484924a84c7cdaed7502bac9f18601cd357f8b1d2/cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463", size = 4926514, upload-time = "2026-03-25T23:33:29.206Z" }, { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" },
{ url = "https://files.pythonhosted.org/packages/34/71/1ea5a7352ae516d5512d17babe7e1b87d9db5150b21f794b1377eac1edc0/cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97", size = 4457766, upload-time = "2026-03-25T23:33:30.834Z" }, { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" },
{ url = "https://files.pythonhosted.org/packages/01/59/562be1e653accee4fdad92c7a2e88fced26b3fdfce144047519bbebc299e/cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c", size = 3986535, upload-time = "2026-03-25T23:33:33.02Z" }, { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" },
{ url = "https://files.pythonhosted.org/packages/d6/8b/b1ebfeb788bf4624d36e45ed2662b8bd43a05ff62157093c1539c1288a18/cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507", size = 4277618, upload-time = "2026-03-25T23:33:34.567Z" }, { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" },
{ url = "https://files.pythonhosted.org/packages/dd/52/a005f8eabdb28df57c20f84c44d397a755782d6ff6d455f05baa2785bd91/cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19", size = 4890802, upload-time = "2026-03-25T23:33:37.034Z" }, { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" },
{ url = "https://files.pythonhosted.org/packages/ec/4d/8e7d7245c79c617d08724e2efa397737715ca0ec830ecb3c91e547302555/cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738", size = 4457425, upload-time = "2026-03-25T23:33:38.904Z" }, { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" },
{ url = "https://files.pythonhosted.org/packages/1d/5c/f6c3596a1430cec6f949085f0e1a970638d76f81c3ea56d93d564d04c340/cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c", size = 4405530, upload-time = "2026-03-25T23:33:40.842Z" }, { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" },
{ url = "https://files.pythonhosted.org/packages/7e/c9/9f9cea13ee2dbde070424e0c4f621c091a91ffcc504ffea5e74f0e1daeff/cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f", size = 4667896, upload-time = "2026-03-25T23:33:42.781Z" }, { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" },
{ url = "https://files.pythonhosted.org/packages/ad/b5/1895bc0821226f129bc74d00eccfc6a5969e2028f8617c09790bf89c185e/cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2", size = 3026348, upload-time = "2026-03-25T23:33:45.021Z" }, { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" },
{ url = "https://files.pythonhosted.org/packages/c3/f8/c9bcbf0d3e6ad288b9d9aa0b1dee04b063d19e8c4f871855a03ab3a297ab/cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124", size = 3483896, upload-time = "2026-03-25T23:33:46.649Z" }, { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" },
{ url = "https://files.pythonhosted.org/packages/01/41/3a578f7fd5c70611c0aacba52cd13cb364a5dee895a5c1d467208a9380b0/cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275", size = 7117147, upload-time = "2026-03-25T23:33:48.249Z" }, { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" },
{ url = "https://files.pythonhosted.org/packages/fa/87/887f35a6fca9dde90cad08e0de0c89263a8e59b2d2ff904fd9fcd8025b6f/cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4", size = 4266221, upload-time = "2026-03-25T23:33:49.874Z" }, { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" },
{ url = "https://files.pythonhosted.org/packages/aa/a8/0a90c4f0b0871e0e3d1ed126aed101328a8a57fd9fd17f00fb67e82a51ca/cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b", size = 4408952, upload-time = "2026-03-25T23:33:52.128Z" }, { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" },
{ url = "https://files.pythonhosted.org/packages/16/0b/b239701eb946523e4e9f329336e4ff32b1247e109cbab32d1a7b61da8ed7/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707", size = 4270141, upload-time = "2026-03-25T23:33:54.11Z" }, { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" },
{ url = "https://files.pythonhosted.org/packages/0f/a8/976acdd4f0f30df7b25605f4b9d3d89295351665c2091d18224f7ad5cdbf/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361", size = 4904178, upload-time = "2026-03-25T23:33:55.725Z" }, { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" },
{ url = "https://files.pythonhosted.org/packages/b1/1b/bf0e01a88efd0e59679b69f42d4afd5bced8700bb5e80617b2d63a3741af/cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b", size = 4441812, upload-time = "2026-03-25T23:33:57.364Z" }, { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/bb/8b/11df86de2ea389c65aa1806f331cae145f2ed18011f30234cc10ca253de8/cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca", size = 3963923, upload-time = "2026-03-25T23:33:59.361Z" }, { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" },
{ url = "https://files.pythonhosted.org/packages/91/e0/207fb177c3a9ef6a8108f234208c3e9e76a6aa8cf20d51932916bd43bda0/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013", size = 4269695, upload-time = "2026-03-25T23:34:00.909Z" }, { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" },
{ url = "https://files.pythonhosted.org/packages/21/5e/19f3260ed1e95bced52ace7501fabcd266df67077eeb382b79c81729d2d3/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4", size = 4869785, upload-time = "2026-03-25T23:34:02.796Z" }, { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" },
{ url = "https://files.pythonhosted.org/packages/10/38/cd7864d79aa1d92ef6f1a584281433419b955ad5a5ba8d1eb6c872165bcb/cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a", size = 4441404, upload-time = "2026-03-25T23:34:04.35Z" }, { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" },
{ url = "https://files.pythonhosted.org/packages/09/0a/4fe7a8d25fed74419f91835cf5829ade6408fd1963c9eae9c4bce390ecbb/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d", size = 4397549, upload-time = "2026-03-25T23:34:06.342Z" }, { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" },
{ url = "https://files.pythonhosted.org/packages/5f/a0/7d738944eac6513cd60a8da98b65951f4a3b279b93479a7e8926d9cd730b/cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736", size = 4651874, upload-time = "2026-03-25T23:34:07.916Z" }, { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" },
{ url = "https://files.pythonhosted.org/packages/cb/f1/c2326781ca05208845efca38bf714f76939ae446cd492d7613808badedf1/cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed", size = 3001511, upload-time = "2026-03-25T23:34:09.892Z" }, { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" },
{ url = "https://files.pythonhosted.org/packages/c9/57/fe4a23eb549ac9d903bd4698ffda13383808ef0876cc912bcb2838799ece/cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4", size = 3471692, upload-time = "2026-03-25T23:34:11.613Z" }, { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/c4/cc/f330e982852403da79008552de9906804568ae9230da8432f7496ce02b71/cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a", size = 7162776, upload-time = "2026-03-25T23:34:13.308Z" }, { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" },
{ url = "https://files.pythonhosted.org/packages/49/b3/dc27efd8dcc4bff583b3f01d4a3943cd8b5821777a58b3a6a5f054d61b79/cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8", size = 4270529, upload-time = "2026-03-25T23:34:15.019Z" }, { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" },
{ url = "https://files.pythonhosted.org/packages/e6/05/e8d0e6eb4f0d83365b3cb0e00eb3c484f7348db0266652ccd84632a3d58d/cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77", size = 4414827, upload-time = "2026-03-25T23:34:16.604Z" }, { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" },
{ url = "https://files.pythonhosted.org/packages/2f/97/daba0f5d2dc6d855e2dcb70733c812558a7977a55dd4a6722756628c44d1/cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290", size = 4271265, upload-time = "2026-03-25T23:34:18.586Z" }, { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" },
{ url = "https://files.pythonhosted.org/packages/89/06/fe1fce39a37ac452e58d04b43b0855261dac320a2ebf8f5260dd55b201a9/cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410", size = 4916800, upload-time = "2026-03-25T23:34:20.561Z" }, { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" },
{ url = "https://files.pythonhosted.org/packages/ff/8a/b14f3101fe9c3592603339eb5d94046c3ce5f7fc76d6512a2d40efd9724e/cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d", size = 4448771, upload-time = "2026-03-25T23:34:22.406Z" }, { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" },
{ url = "https://files.pythonhosted.org/packages/01/b3/0796998056a66d1973fd52ee89dc1bb3b6581960a91ad4ac705f182d398f/cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70", size = 3978333, upload-time = "2026-03-25T23:34:24.281Z" }, { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" },
{ url = "https://files.pythonhosted.org/packages/c5/3d/db200af5a4ffd08918cd55c08399dc6c9c50b0bc72c00a3246e099d3a849/cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d", size = 4271069, upload-time = "2026-03-25T23:34:25.895Z" }, { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" },
{ url = "https://files.pythonhosted.org/packages/d7/18/61acfd5b414309d74ee838be321c636fe71815436f53c9f0334bf19064fa/cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa", size = 4878358, upload-time = "2026-03-25T23:34:27.67Z" }, { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" },
{ url = "https://files.pythonhosted.org/packages/8b/65/5bf43286d566f8171917cae23ac6add941654ccf085d739195a4eacf1674/cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58", size = 4448061, upload-time = "2026-03-25T23:34:29.375Z" }, { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" },
{ url = "https://files.pythonhosted.org/packages/e0/25/7e49c0fa7205cf3597e525d156a6bce5b5c9de1fd7e8cb01120e459f205a/cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb", size = 4399103, upload-time = "2026-03-25T23:34:32.036Z" }, { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" },
{ url = "https://files.pythonhosted.org/packages/44/46/466269e833f1c4718d6cd496ffe20c56c9c8d013486ff66b4f69c302a68d/cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72", size = 4659255, upload-time = "2026-03-25T23:34:33.679Z" }, { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" },
{ url = "https://files.pythonhosted.org/packages/0a/09/ddc5f630cc32287d2c953fc5d32705e63ec73e37308e5120955316f53827/cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c", size = 3010660, upload-time = "2026-03-25T23:34:35.418Z" }, { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" },
{ url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" },
] ]
[[package]] [[package]]
@@ -545,14 +545,14 @@ wheels = [
[[package]] [[package]]
name = "dj-database-url" name = "dj-database-url"
version = "3.1.2" version = "3.1.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/03/f6/00b625e9d371b980aa261011d0dc906a16444cb688f94215e0dc86996eb5/dj_database_url-3.1.2.tar.gz", hash = "sha256:63c20e4bbaa51690dfd4c8d189521f6bf6bc9da9fcdb23d95d2ee8ee87f9ec62", size = 11490, upload-time = "2026-02-19T15:30:23.638Z" } sdist = { url = "https://files.pythonhosted.org/packages/95/c6/88676a7333fb7c668e626b55f8bfc8527dd863973eb1c40412b95d27747d/dj_database_url-3.1.0.tar.gz", hash = "sha256:d80218426b83f9302c8d27d4fccf52de5cf0cab179f0645fb2839f37605d1353", size = 7924, upload-time = "2026-01-04T09:18:32.693Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/a9/57c66006373381f1d3e5bd94216f1d371228a89f443d3030e010f73dd198/dj_database_url-3.1.2-py3-none-any.whl", hash = "sha256:544e015fee3efa5127a1eb1cca465f4ace578265b3671fe61d0ed7dbafb5ec8a", size = 8953, upload-time = "2026-02-19T15:30:39.37Z" }, { url = "https://files.pythonhosted.org/packages/68/1b/e84f7472ab0bdacc3fd09556eb4dd40d88246941d465cc103b36a8dabcd8/dj_database_url-3.1.0-py3-none-any.whl", hash = "sha256:155a56fbbecbaaf1348ccd73bf29138b4c9988363ba08261a0f0145e392e638c", size = 8849, upload-time = "2026-01-04T09:18:43.77Z" },
] ]
[[package]] [[package]]
@@ -633,7 +633,7 @@ wheels = [
[[package]] [[package]]
name = "django-lasuite" name = "django-lasuite"
version = "0.0.25" version = "0.0.24"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
@@ -644,9 +644,9 @@ dependencies = [
{ name = "requests" }, { name = "requests" },
{ name = "requests-toolbelt" }, { name = "requests-toolbelt" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/5e/bc/5826a3e5ada5dad1ff6af9a2e1eb598a0dc2cb9c4d2dbf011f1bbb310c9e/django_lasuite-0.0.25.tar.gz", hash = "sha256:ee44783942e6ead74a732f6d7280c5fca961b66581350ce01c0589c3e80684cf", size = 34819, upload-time = "2026-03-10T13:40:28.705Z" } sdist = { url = "https://files.pythonhosted.org/packages/53/dc/4002ba20b964237a609628d53b950c5aecf6387119058831e0ee33176f59/django_lasuite-0.0.24.tar.gz", hash = "sha256:3231b0178a2187405c8faae447225c5dd069263a9a09e73e2a36427be4bf2388", size = 34293, upload-time = "2026-02-11T12:25:42.558Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/2f/819ebbc9d3a4b8233cef1e9b0b607724e4acf527988393b04a4036b9c235/django_lasuite-0.0.25-py3-none-any.whl", hash = "sha256:4c10f625005cd41d05e8d34269f1a3b58fea8be5296528bd58596227f50d9884", size = 54034, upload-time = "2026-03-10T13:40:26.903Z" }, { url = "https://files.pythonhosted.org/packages/5d/90/9170b8f443ab5d34b32d7cd386ce3b38ca35baae966da4a1d77b4f4ab3d2/django_lasuite-0.0.24-py3-none-any.whl", hash = "sha256:c37e0b3606a23fd4094ca435fa4edf7a64d325be64b0310fa6577f75d515d856", size = 52620, upload-time = "2026-02-11T12:25:40.626Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -737,11 +737,11 @@ wheels = [
[[package]] [[package]]
name = "dockerflow" name = "dockerflow"
version = "2026.3.4" version = "2026.1.26"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/03/935f8399094611981b37634a7ee078484b3995f744135339e51385638f6c/dockerflow-2026.3.4.tar.gz", hash = "sha256:6cc7fcab2a078d06aeca8b02e22fd7f6766dca322fb30aed6910684dbc08690c", size = 68701, upload-time = "2026-03-04T09:01:28.178Z" } sdist = { url = "https://files.pythonhosted.org/packages/11/08/7abe8458ce6fa82e694ba9b792b4158596fd681782976699d3f15642a23a/dockerflow-2026.1.26.tar.gz", hash = "sha256:c45667ec078944a468b9a634c866587b4cf31559d0e6f369fbadd21edce66b38", size = 68042, upload-time = "2026-01-26T15:30:06.606Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/86/b9/2d7bf30eb0c24e60e61e4f6ae7574e123d3fa51b39a612e146c94158446a/dockerflow-2026.3.4-py2.py3-none-any.whl", hash = "sha256:38fe0ad30a4aedf68e589919695d502fc6f584a761b56a91f56e80cbadfbf61a", size = 36424, upload-time = "2026-03-04T09:01:26.631Z" }, { url = "https://files.pythonhosted.org/packages/de/88/26cec1e8ce09b4022233f232962fba72650c22941d7a609d10b9eec9f748/dockerflow-2026.1.26-py2.py3-none-any.whl", hash = "sha256:26f02d8842672921b2a49f1e019fac54ed2ee69dc673ed80f7b237b67394c2be", size = 35992, upload-time = "2026-01-26T15:30:05.525Z" },
] ]
[[package]] [[package]]
@@ -763,14 +763,14 @@ wheels = [
[[package]] [[package]]
name = "drf-spectacular-sidecar" name = "drf-spectacular-sidecar"
version = "2026.3.1" version = "2026.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "django" }, { name = "django" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/aa/42/2f8c1b2846399d47094ec414bc0d6a7cce7ba95fd6545a97285eee89f7f1/drf_spectacular_sidecar-2026.3.1.tar.gz", hash = "sha256:5b7fedad66e3851f2f442480792c08115d79217959d01645b93d3d2258938be1", size = 2461501, upload-time = "2026-03-01T11:31:19.708Z" } sdist = { url = "https://files.pythonhosted.org/packages/1e/81/c7b0e3ccbd5a039c4f4fcfecf88391a666ca1406a953886e2f39295b1c90/drf_spectacular_sidecar-2026.1.1.tar.gz", hash = "sha256:6f7c173a8ddbbbdafc7a27e028614b65f07a89ca90f996a432d57460463b56be", size = 2468060, upload-time = "2026-01-01T11:27:12.682Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/28/2d5e64d101ebc5180674fcaf7b5a35e398e2f8d9688b2e8d52b0e1394e7d/drf_spectacular_sidecar-2026.3.1-py3-none-any.whl", hash = "sha256:864edb83e022e13e3941c325c3cc0c954c843fa2e1d0bc95e81887664b2d3dad", size = 2481725, upload-time = "2026-03-01T11:31:18.469Z" }, { url = "https://files.pythonhosted.org/packages/db/96/38725edda526f3e9e597f531beeec94b0ef433d9494f06a13b7636eecb6e/drf_spectacular_sidecar-2026.1.1-py3-none-any.whl", hash = "sha256:af8df62f1b594ec280351336d837eaf2402ab25a6bc2a1fad7aee9935821070f", size = 2489520, upload-time = "2026-01-01T11:27:11.056Z" },
] ]
[[package]] [[package]]
@@ -976,7 +976,7 @@ wheels = [
[[package]] [[package]]
name = "ipython" name = "ipython"
version = "9.11.0" version = "9.10.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" }, { name = "colorama", marker = "sys_platform == 'win32'" },
@@ -990,9 +990,9 @@ dependencies = [
{ name = "stack-data" }, { name = "stack-data" },
{ name = "traitlets" }, { name = "traitlets" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/86/28/a4698eda5a8928a45d6b693578b135b753e14fa1c2b36ee9441e69a45576/ipython-9.11.0.tar.gz", hash = "sha256:2a94bc4406b22ecc7e4cb95b98450f3ea493a76bec8896cda11b78d7752a6667", size = 4427354, upload-time = "2026-03-05T08:57:30.549Z" } sdist = { url = "https://files.pythonhosted.org/packages/a6/60/2111715ea11f39b1535bed6024b7dec7918b71e5e5d30855a5b503056b50/ipython-9.10.0.tar.gz", hash = "sha256:cd9e656be97618a0676d058134cd44e6dc7012c0e5cb36a9ce96a8c904adaf77", size = 4426526, upload-time = "2026-02-02T10:00:33.594Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/b2/90/45c72becc57158facc6a6404f663b77bbcea2519ca57f760e2879ae1315d/ipython-9.11.0-py3-none-any.whl", hash = "sha256:6922d5bcf944c6e525a76a0a304451b60a2b6f875e86656d8bc2dfda5d710e19", size = 624222, upload-time = "2026-03-05T08:57:28.94Z" }, { url = "https://files.pythonhosted.org/packages/3d/aa/898dec789a05731cd5a9f50605b7b44a72bd198fd0d4528e11fc610177cc/ipython-9.10.0-py3-none-any.whl", hash = "sha256:c6ab68cc23bba8c7e18e9b932797014cc61ea7fd6f19de180ab9ba73e65ee58d", size = 622774, upload-time = "2026-02-02T10:00:31.503Z" },
] ]
[[package]] [[package]]
@@ -1157,7 +1157,7 @@ wheels = [
[[package]] [[package]]
name = "meet" name = "meet"
version = "1.13.0" version = "1.10.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "aiohttp" }, { name = "aiohttp" },
@@ -1221,25 +1221,25 @@ dev = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "aiohttp", specifier = "==3.13.4" }, { name = "aiohttp", specifier = "==3.13.3" },
{ name = "boto3", specifier = "==1.42.68" }, { name = "boto3", specifier = "==1.42.49" },
{ name = "brevo-python", specifier = "==1.2.0" }, { name = "brevo-python", specifier = "==1.2.0" },
{ name = "brotli", specifier = "==1.2.0" }, { name = "brotli", specifier = "==1.2.0" },
{ name = "celery", extras = ["redis"], specifier = "==5.6.2" }, { name = "celery", extras = ["redis"], specifier = "==5.6.2" },
{ name = "dj-database-url", specifier = "==3.1.2" }, { name = "dj-database-url", specifier = "==3.1.0" },
{ name = "django", specifier = "==5.2.12" }, { name = "django", specifier = "==5.2.12" },
{ name = "django-configurations", specifier = "==2.5.1" }, { name = "django-configurations", specifier = "==2.5.1" },
{ name = "django-cors-headers", specifier = "==4.9.0" }, { name = "django-cors-headers", specifier = "==4.9.0" },
{ name = "django-countries", specifier = "==8.2.0" }, { name = "django-countries", specifier = "==8.2.0" },
{ name = "django-filter", specifier = "==25.2" }, { name = "django-filter", specifier = "==25.2" },
{ name = "django-lasuite", extras = ["all"], specifier = "==0.0.25" }, { name = "django-lasuite", extras = ["all"], specifier = "==0.0.24" },
{ name = "django-parler", specifier = "==2.3" }, { name = "django-parler", specifier = "==2.3" },
{ name = "django-pydantic-field", specifier = "==0.5.4" }, { name = "django-pydantic-field", specifier = "==0.5.4" },
{ name = "django-redis", specifier = "==6.0.0" }, { name = "django-redis", specifier = "==6.0.0" },
{ name = "django-storages", extras = ["s3"], specifier = "==1.14.6" }, { name = "django-storages", extras = ["s3"], specifier = "==1.14.6" },
{ name = "django-timezone-field", specifier = ">=5.1" }, { name = "django-timezone-field", specifier = ">=5.1" },
{ name = "djangorestframework", specifier = "==3.16.1" }, { name = "djangorestframework", specifier = "==3.16.1" },
{ name = "dockerflow", specifier = "==2026.3.4" }, { name = "dockerflow", specifier = "==2026.1.26" },
{ name = "drf-spectacular", specifier = "==0.29.0" }, { name = "drf-spectacular", specifier = "==0.29.0" },
{ name = "easy-thumbnails", specifier = "==2.10.1" }, { name = "easy-thumbnails", specifier = "==2.10.1" },
{ name = "factory-boy", specifier = "==3.3.3" }, { name = "factory-boy", specifier = "==3.3.3" },
@@ -1249,25 +1249,25 @@ requires-dist = [
{ name = "markdown", specifier = "==3.10.2" }, { name = "markdown", specifier = "==3.10.2" },
{ name = "mozilla-django-oidc", specifier = "==5.0.2" }, { name = "mozilla-django-oidc", specifier = "==5.0.2" },
{ name = "nested-multipart-parser", specifier = "==1.6.0" }, { name = "nested-multipart-parser", specifier = "==1.6.0" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.3" }, { name = "psycopg", extras = ["binary"], specifier = "==3.3.2" },
{ name = "pydantic", specifier = "==2.12.5" }, { name = "pydantic", specifier = "==2.12.4" },
{ name = "pyjwt", specifier = "==2.12.1" }, { name = "pyjwt", specifier = "==2.11.0" },
{ name = "python-frontmatter", specifier = "==1.1.0" }, { name = "python-frontmatter", specifier = "==1.1.0" },
{ name = "python-magic", specifier = "==0.4.27" }, { name = "python-magic", specifier = "==0.4.27" },
{ name = "redis", specifier = "==5.2.1" }, { name = "redis", specifier = "==5.2.1" },
{ name = "requests", specifier = "==2.33.0" }, { name = "requests", specifier = "==2.32.5" },
{ name = "sentry-sdk", specifier = "==2.54.0" }, { name = "sentry-sdk", specifier = "==2.53.0" },
{ name = "whitenoise", specifier = "==6.12.0" }, { name = "whitenoise", specifier = "==6.11.0" },
] ]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "django-extensions", specifier = "==4.1" }, { name = "django-extensions", specifier = "==4.1" },
{ name = "drf-spectacular-sidecar", specifier = "==2026.3.1" }, { name = "drf-spectacular-sidecar", specifier = "==2026.1.1" },
{ name = "freezegun", specifier = "==1.5.5" }, { name = "freezegun", specifier = "==1.5.5" },
{ name = "ipdb", specifier = "==0.13.13" }, { name = "ipdb", specifier = "==0.13.13" },
{ name = "ipython", specifier = "==9.11.0" }, { name = "ipython", specifier = "==9.10.0" },
{ name = "pyfakefs", specifier = "==6.1.5" }, { name = "pyfakefs", specifier = "==6.1.1" },
{ name = "pylint", specifier = "<4.0.0" }, { name = "pylint", specifier = "<4.0.0" },
{ name = "pylint-django", specifier = "==2.7.0" }, { name = "pylint-django", specifier = "==2.7.0" },
{ name = "pytest", specifier = "==9.0.2" }, { name = "pytest", specifier = "==9.0.2" },
@@ -1275,8 +1275,8 @@ dev = [
{ name = "pytest-django", specifier = "==4.12.0" }, { name = "pytest-django", specifier = "==4.12.0" },
{ name = "pytest-icdiff", specifier = "==0.9" }, { name = "pytest-icdiff", specifier = "==0.9" },
{ name = "pytest-xdist", specifier = "==3.8.0" }, { name = "pytest-xdist", specifier = "==3.8.0" },
{ name = "responses", specifier = "==0.26.0" }, { name = "responses", specifier = "==0.25.8" },
{ name = "ruff", specifier = "==0.15.6" }, { name = "ruff", specifier = "==0.15.1" },
{ name = "types-requests", specifier = "==2.32.4.20260107" }, { name = "types-requests", specifier = "==2.32.4.20260107" },
] ]
@@ -1595,14 +1595,14 @@ wheels = [
[[package]] [[package]]
name = "psycopg" name = "psycopg"
version = "3.3.3" version = "3.3.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "tzdata", marker = "sys_platform == 'win32'" }, { name = "tzdata", marker = "sys_platform == 'win32'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/d3/b6/379d0a960f8f435ec78720462fd94c4863e7a31237cf81bf76d0af5883bf/psycopg-3.3.3.tar.gz", hash = "sha256:5e9a47458b3c1583326513b2556a2a9473a1001a56c9efe9e587245b43148dd9", size = 165624, upload-time = "2026-02-18T16:52:16.546Z" } sdist = { url = "https://files.pythonhosted.org/packages/e0/1a/7d9ef4fdc13ef7f15b934c393edc97a35c281bb7d3c3329fbfcbe915a7c2/psycopg-3.3.2.tar.gz", hash = "sha256:707a67975ee214d200511177a6a80e56e654754c9afca06a7194ea6bbfde9ca7", size = 165630, upload-time = "2025-12-06T17:34:53.899Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" }, { url = "https://files.pythonhosted.org/packages/8c/51/2779ccdf9305981a06b21a6b27e8547c948d85c41c76ff434192784a4c93/psycopg-3.3.2-py3-none-any.whl", hash = "sha256:3e94bc5f4690247d734599af56e51bae8e0db8e4311ea413f801fef82b14a99b", size = 212774, upload-time = "2025-12-06T17:31:41.414Z" },
] ]
[package.optional-dependencies] [package.optional-dependencies]
@@ -1612,31 +1612,31 @@ binary = [
[[package]] [[package]]
name = "psycopg-binary" name = "psycopg-binary"
version = "3.3.3" version = "3.3.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" }, { url = "https://files.pythonhosted.org/packages/14/73/7ca7cb22b9ac7393fb5de7d28ca97e8347c375c8498b3bff2c99c1f38038/psycopg_binary-3.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fc5a189e89cbfff174588665bb18d28d2d0428366cc9dae5864afcaa2e57380b", size = 4579068, upload-time = "2025-12-06T17:33:39.303Z" },
{ url = "https://files.pythonhosted.org/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" }, { url = "https://files.pythonhosted.org/packages/f5/42/0cf38ff6c62c792fc5b55398a853a77663210ebd51ed6f0c4a05b06f95a6/psycopg_binary-3.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:083c2e182be433f290dc2c516fd72b9b47054fcd305cce791e0a50d9e93e06f2", size = 4657520, upload-time = "2025-12-06T17:33:42.536Z" },
{ url = "https://files.pythonhosted.org/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" }, { url = "https://files.pythonhosted.org/packages/3b/60/df846bc84cbf2231e01b0fff48b09841fe486fa177665e50f4995b1bfa44/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:ac230e3643d1c436a2dfb59ca84357dfc6862c9f372fc5dbd96bafecae581f9f", size = 5452086, upload-time = "2025-12-06T17:33:46.54Z" },
{ url = "https://files.pythonhosted.org/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" }, { url = "https://files.pythonhosted.org/packages/ab/85/30c846a00db86b1b53fd5bfd4b4edfbd0c00de8f2c75dd105610bd7568fc/psycopg_binary-3.3.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d8c899a540f6c7585cee53cddc929dd4d2db90fd828e37f5d4017b63acbc1a5d", size = 5131125, upload-time = "2025-12-06T17:33:50.413Z" },
{ url = "https://files.pythonhosted.org/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" }, { url = "https://files.pythonhosted.org/packages/6d/15/9968732013373f36f8a2a3fb76104dffc8efd9db78709caa5ae1a87b1f80/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50ff10ab8c0abdb5a5451b9315538865b50ba64c907742a1385fdf5f5772b73e", size = 6722914, upload-time = "2025-12-06T17:33:54.544Z" },
{ url = "https://files.pythonhosted.org/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" }, { url = "https://files.pythonhosted.org/packages/b2/ba/29e361fe02143ac5ff5a1ca3e45697344cfbebe2eaf8c4e7eec164bff9a0/psycopg_binary-3.3.2-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23d2594af848c1fd3d874a9364bef50730124e72df7bb145a20cb45e728c50ed", size = 4966081, upload-time = "2025-12-06T17:33:58.477Z" },
{ url = "https://files.pythonhosted.org/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" }, { url = "https://files.pythonhosted.org/packages/99/45/1be90c8f1a1a237046903e91202fb06708745c179f220b361d6333ed7641/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ea4fe6b4ead3bbbe27244ea224fcd1f53cb119afc38b71a2f3ce570149a03e30", size = 4493332, upload-time = "2025-12-06T17:34:02.011Z" },
{ url = "https://files.pythonhosted.org/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" }, { url = "https://files.pythonhosted.org/packages/2e/b5/bbdc07d5f0a5e90c617abd624368182aa131485e18038b2c6c85fc054aed/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:742ce48cde825b8e52fb1a658253d6d1ff66d152081cbc76aa45e2986534858d", size = 4170781, upload-time = "2025-12-06T17:34:05.298Z" },
{ url = "https://files.pythonhosted.org/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" }, { url = "https://files.pythonhosted.org/packages/d1/2a/0d45e4f4da2bd78c3237ffa03475ef3751f69a81919c54a6e610eb1a7c96/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e22bf6b54df994aff37ab52695d635f1ef73155e781eee1f5fa75bc08b58c8da", size = 3910544, upload-time = "2025-12-06T17:34:08.251Z" },
{ url = "https://files.pythonhosted.org/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" }, { url = "https://files.pythonhosted.org/packages/3a/62/a8e0f092f4dbef9a94b032fb71e214cf0a375010692fbe7493a766339e47/psycopg_binary-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8db9034cde3bcdafc66980f0130813f5c5d19e74b3f2a19fb3cfbc25ad113121", size = 4220070, upload-time = "2025-12-06T17:34:11.392Z" },
{ url = "https://files.pythonhosted.org/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" }, { url = "https://files.pythonhosted.org/packages/09/e6/5fc8d8aff8afa114bb4a94a0341b9309311e8bf3ab32d816032f8b984d4e/psycopg_binary-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:df65174c7cf6b05ea273ce955927d3270b3a6e27b0b12762b009ce6082b8d3fc", size = 3540922, upload-time = "2025-12-06T17:34:14.88Z" },
{ url = "https://files.pythonhosted.org/packages/a2/71/7a57e5b12275fe7e7d84d54113f0226080423a869118419c9106c083a21c/psycopg_binary-3.3.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:497852c5eaf1f0c2d88ab74a64a8097c099deac0c71de1cbcf18659a8a04a4b2", size = 4607368, upload-time = "2026-02-18T16:51:19.295Z" }, { url = "https://files.pythonhosted.org/packages/bd/75/ad18c0b97b852aba286d06befb398cc6d383e9dfd0a518369af275a5a526/psycopg_binary-3.3.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9ca24062cd9b2270e4d77576042e9cc2b1d543f09da5aba1f1a3d016cea28390", size = 4596371, upload-time = "2025-12-06T17:34:18.007Z" },
{ url = "https://files.pythonhosted.org/packages/c7/04/cb834f120f2b2c10d4003515ef9ca9d688115b9431735e3936ae48549af8/psycopg_binary-3.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:258d1ea53464d29768bf25930f43291949f4c7becc706f6e220c515a63a24edd", size = 4687047, upload-time = "2026-02-18T16:51:23.84Z" }, { url = "https://files.pythonhosted.org/packages/5a/79/91649d94c8d89f84af5da7c9d474bfba35b08eb8f492ca3422b08f0a6427/psycopg_binary-3.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c749770da0947bc972e512f35366dd4950c0e34afad89e60b9787a37e97cb443", size = 4675139, upload-time = "2025-12-06T17:34:21.374Z" },
{ url = "https://files.pythonhosted.org/packages/40/e9/47a69692d3da9704468041aa5ed3ad6fc7f6bb1a5ae788d261a26bbca6c7/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:111c59897a452196116db12e7f608da472fbff000693a21040e35fc978b23430", size = 5487096, upload-time = "2026-02-18T16:51:29.645Z" }, { url = "https://files.pythonhosted.org/packages/56/ac/b26e004880f054549ec9396594e1ffe435810b0673e428e619ed722e4244/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:03b7cd73fb8c45d272a34ae7249713e32492891492681e3cf11dff9531cf37e9", size = 5456120, upload-time = "2025-12-06T17:34:25.102Z" },
{ url = "https://files.pythonhosted.org/packages/0b/b6/0e0dd6a2f802864a4ae3dbadf4ec620f05e3904c7842b326aafc43e5f464/psycopg_binary-3.3.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:17bb6600e2455993946385249a3c3d0af52cd70c1c1cdbf712e9d696d0b0bf1b", size = 5168720, upload-time = "2026-02-18T16:51:36.499Z" }, { url = "https://files.pythonhosted.org/packages/4b/8d/410681dccd6f2999fb115cc248521ec50dd2b0aba66ae8de7e81efdebbee/psycopg_binary-3.3.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:43b130e3b6edcb5ee856c7167ccb8561b473308c870ed83978ae478613764f1c", size = 5133484, upload-time = "2025-12-06T17:34:28.933Z" },
{ url = "https://files.pythonhosted.org/packages/6f/0d/977af38ac19a6b55d22dff508bd743fd7c1901e1b73657e7937c7cccb0a3/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:642050398583d61c9856210568eb09a8e4f2fe8224bf3be21b67a370e677eead", size = 6762076, upload-time = "2026-02-18T16:51:43.167Z" }, { url = "https://files.pythonhosted.org/packages/66/30/ebbab99ea2cfa099d7b11b742ce13415d44f800555bfa4ad2911dc645b71/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1feba5a8c617922321aef945865334e468337b8fc5c73074f5e63143013b5a", size = 6731818, upload-time = "2025-12-06T17:34:33.094Z" },
{ url = "https://files.pythonhosted.org/packages/34/40/912a39d48322cf86895c0eaf2d5b95cb899402443faefd4b09abbba6b6e1/psycopg_binary-3.3.3-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:533efe6dc3a7cba5e2a84e38970786bb966306863e45f3db152007e9f48638a6", size = 4997623, upload-time = "2026-02-18T16:51:47.707Z" }, { url = "https://files.pythonhosted.org/packages/70/02/d260646253b7ad805d60e0de47f9b811d6544078452579466a098598b6f4/psycopg_binary-3.3.2-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cabb2a554d9a0a6bf84037d86ca91782f087dfff2a61298d0b00c19c0bc43f6d", size = 4983859, upload-time = "2025-12-06T17:34:36.457Z" },
{ url = "https://files.pythonhosted.org/packages/98/0c/c14d0e259c65dc7be854d926993f151077887391d5a081118907a9d89603/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5958dbf28b77ce2033482f6cb9ef04d43f5d8f4b7636e6963d5626f000efb23e", size = 4532096, upload-time = "2026-02-18T16:51:51.421Z" }, { url = "https://files.pythonhosted.org/packages/72/8d/e778d7bad1a7910aa36281f092bd85c5702f508fd9bb0ea2020ffbb6585c/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74bc306c4b4df35b09bc8cecf806b271e1c5d708f7900145e4e54a2e5dedfed0", size = 4516388, upload-time = "2025-12-06T17:34:40.129Z" },
{ url = "https://files.pythonhosted.org/packages/39/21/8b7c50a194cfca6ea0fd4d1f276158307785775426e90700ab2eba5cd623/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a6af77b6626ce92b5817bf294b4d45ec1a6161dba80fc2d82cdffdd6814fd023", size = 4208884, upload-time = "2026-02-18T16:51:57.336Z" }, { url = "https://files.pythonhosted.org/packages/bd/f1/64e82098722e2ab3521797584caf515284be09c1e08a872551b6edbb0074/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:d79b0093f0fbf7a962d6a46ae292dc056c65d16a8ee9361f3cfbafd4c197ab14", size = 4192382, upload-time = "2025-12-06T17:34:43.279Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2c/a4981bf42cf30ebba0424971d7ce70a222ae9b82594c42fc3f2105d7b525/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:47f06fcbe8542b4d96d7392c476a74ada521c5aebdb41c3c0155f6595fc14c8d", size = 3944542, upload-time = "2026-02-18T16:52:04.266Z" }, { url = "https://files.pythonhosted.org/packages/fa/d0/c20f4e668e89494972e551c31be2a0016e3f50d552d7ae9ac07086407599/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:1586e220be05547c77afc326741dd41cc7fba38a81f9931f616ae98865439678", size = 3928660, upload-time = "2025-12-06T17:34:46.757Z" },
{ url = "https://files.pythonhosted.org/packages/60/e9/b7c29b56aa0b85a4e0c4d89db691c1ceef08f46a356369144430c155a2f5/psycopg_binary-3.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e7800e6c6b5dc4b0ca7cc7370f770f53ac83886b76afda0848065a674231e856", size = 4254339, upload-time = "2026-02-18T16:52:10.444Z" }, { url = "https://files.pythonhosted.org/packages/0f/e1/99746c171de22539fd5eb1c9ca21dc805b54cfae502d7451d237d1dbc349/psycopg_binary-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:458696a5fa5dad5b6fb5d5862c22454434ce4fe1cf66ca6c0de5f904cbc1ae3e", size = 4239169, upload-time = "2025-12-06T17:34:49.751Z" },
{ url = "https://files.pythonhosted.org/packages/98/5a/291d89f44d3820fffb7a04ebc8f3ef5dda4f542f44a5daea0c55a84abf45/psycopg_binary-3.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:165f22ab5a9513a3d7425ffb7fcc7955ed8ccaeef6d37e369d6cc1dff1582383", size = 3652796, upload-time = "2026-02-18T16:52:14.02Z" }, { url = "https://files.pythonhosted.org/packages/72/f7/212343c1c9cfac35fd943c527af85e9091d633176e2a407a0797856ff7b9/psycopg_binary-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:04bb2de4ba69d6f8395b446ede795e8884c040ec71d01dd07ac2b2d18d4153d1", size = 3642122, upload-time = "2025-12-06T17:34:52.506Z" },
] ]
[[package]] [[package]]
@@ -1668,7 +1668,7 @@ wheels = [
[[package]] [[package]]
name = "pydantic" name = "pydantic"
version = "2.12.5" version = "2.12.4"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "annotated-types" }, { name = "annotated-types" },
@@ -1676,9 +1676,9 @@ dependencies = [
{ name = "typing-extensions" }, { name = "typing-extensions" },
{ name = "typing-inspection" }, { name = "typing-inspection" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } sdist = { url = "https://files.pythonhosted.org/packages/96/ad/a17bc283d7d81837c061c49e3eaa27a45991759a1b7eae1031921c6bd924/pydantic-2.12.4.tar.gz", hash = "sha256:0f8cb9555000a4b5b617f66bfd2566264c4984b27589d3b845685983e8ea85ac", size = 821038, upload-time = "2025-11-05T10:50:08.59Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, { url = "https://files.pythonhosted.org/packages/82/2f/e68750da9b04856e2a7ec56fc6f034a5a79775e9b9a81882252789873798/pydantic-2.12.4-py3-none-any.whl", hash = "sha256:92d3d202a745d46f9be6df459ac5a064fdaa3c1c4cd8adcfa332ccf3c05f871e", size = 463400, upload-time = "2025-11-05T10:50:06.732Z" },
] ]
[[package]] [[package]]
@@ -1736,11 +1736,11 @@ wheels = [
[[package]] [[package]]
name = "pyfakefs" name = "pyfakefs"
version = "6.1.5" version = "6.1.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/87/fe3f1fbbf18480b57e6b195d4d416e01d8e55bf88743982ccb3a8845c2a2/pyfakefs-6.1.5.tar.gz", hash = "sha256:d70fb1c622a1d5c729462d31bb58eead99c1830986bc049bf8975c480a49d19b", size = 226814, upload-time = "2026-03-15T19:05:18.025Z" } sdist = { url = "https://files.pythonhosted.org/packages/43/84/843a50cfde48d6004cbf540b38b82a39737273cc76b51da6b456fea75de0/pyfakefs-6.1.1.tar.gz", hash = "sha256:1c166fdba85f3f288f89b2a410e05712914d1f7ea77b920e960fe0cbbea1294d", size = 226032, upload-time = "2026-02-09T19:44:19.539Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/d8/a60fb94ab2b5a9bf0e38d6e93667904d4e844d8b7e9cfb5ef0304d68da6b/pyfakefs-6.1.5-py3-none-any.whl", hash = "sha256:2c482f36bad7d1f7516d58d17818fb0dbe397a3372ec5274869386a9b7cd4883", size = 239885, upload-time = "2026-03-15T19:05:16.488Z" }, { url = "https://files.pythonhosted.org/packages/a5/e2/1d74c7663aa38e405c25a30c26e8a1b92c06f8947118b9617042f4290ade/pyfakefs-6.1.1-py3-none-any.whl", hash = "sha256:e2c30fd0b7736ba03785bbc20b86de2312623cf7a11a7de334042ab0de23d46f", size = 239630, upload-time = "2026-02-09T19:44:18.093Z" },
] ]
[[package]] [[package]]
@@ -1754,11 +1754,11 @@ wheels = [
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.12.1" version = "2.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c2/27/a3b6e5bf6ff856d2509292e95c8f57f0df7017cf5394921fc4e4ef40308a/pyjwt-2.12.1.tar.gz", hash = "sha256:c74a7a2adf861c04d002db713dd85f84beb242228e671280bf709d765b03672b", size = 102564, upload-time = "2026-03-13T19:27:37.25Z" } sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019, upload-time = "2026-01-30T19:59:55.694Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/7a/8dd906bd22e79e47397a61742927f6747fe93242ef86645ee9092e610244/pyjwt-2.12.1-py3-none-any.whl", hash = "sha256:28ca37c070cad8ba8cd9790cd940535d40274d22f80ab87f3ac6a713e6e8454c", size = 29726, upload-time = "2026-03-13T19:27:35.677Z" }, { url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224, upload-time = "2026-01-30T19:59:54.539Z" },
] ]
[[package]] [[package]]
@@ -1965,7 +1965,7 @@ wheels = [
[[package]] [[package]]
name = "requests" name = "requests"
version = "2.33.0" version = "2.32.5"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
@@ -1973,9 +1973,9 @@ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" },
] ]
[[package]] [[package]]
@@ -1992,16 +1992,16 @@ wheels = [
[[package]] [[package]]
name = "responses" name = "responses"
version = "0.26.0" version = "0.25.8"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pyyaml" }, { name = "pyyaml" },
{ name = "requests" }, { name = "requests" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/9f/b4/b7e040379838cc71bf5aabdb26998dfbe5ee73904c92c1c161faf5de8866/responses-0.26.0.tar.gz", hash = "sha256:c7f6923e6343ef3682816ba421c006626777893cb0d5e1434f674b649bac9eb4", size = 81303, upload-time = "2026-02-19T14:38:05.574Z" } sdist = { url = "https://files.pythonhosted.org/packages/0e/95/89c054ad70bfef6da605338b009b2e283485835351a9935c7bfbfaca7ffc/responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4", size = 79320, upload-time = "2025-08-08T19:01:46.709Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/04/7f73d05b556da048923e31a0cc878f03be7c5425ed1f268082255c75d872/responses-0.26.0-py3-none-any.whl", hash = "sha256:03ec4409088cd5c66b71ecbbbd27fe2c58ddfad801c66203457b3e6a04868c37", size = 35099, upload-time = "2026-02-19T14:38:03.847Z" }, { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" },
] ]
[[package]] [[package]]
@@ -2072,27 +2072,27 @@ wheels = [
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.15.6" version = "0.15.1"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, { url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
{ url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, { url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
{ url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, { url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
{ url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, { url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
{ url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, { url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
{ url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, { url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
{ url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, { url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
{ url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, { url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
{ url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, { url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
{ url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, { url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
{ url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, { url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
{ url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, { url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
{ url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, { url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
{ url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, { url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
{ url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, { url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
{ url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, { url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
{ url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, { url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
] ]
[[package]] [[package]]
@@ -2109,15 +2109,15 @@ wheels = [
[[package]] [[package]]
name = "sentry-sdk" name = "sentry-sdk"
version = "2.54.0" version = "2.53.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "certifi" }, { name = "certifi" },
{ name = "urllib3" }, { name = "urllib3" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c8/e9/2e3a46c304e7fa21eaa70612f60354e32699c7102eb961f67448e222ad7c/sentry_sdk-2.54.0.tar.gz", hash = "sha256:2620c2575128d009b11b20f7feb81e4e4e8ae08ec1d36cbc845705060b45cc1b", size = 413813, upload-time = "2026-03-02T15:12:41.355Z" } sdist = { url = "https://files.pythonhosted.org/packages/d3/06/66c8b705179bc54087845f28fd1b72f83751b6e9a195628e2e9af9926505/sentry_sdk-2.53.0.tar.gz", hash = "sha256:6520ef2c4acd823f28efc55e43eb6ce2e6d9f954a95a3aa96b6fd14871e92b77", size = 412369, upload-time = "2026-02-16T11:11:14.743Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/39/be412cc86bc6247b8f69e9383d7950711bd86f8d0a4a4b0fe8fad685bc21/sentry_sdk-2.54.0-py2.py3-none-any.whl", hash = "sha256:fd74e0e281dcda63afff095d23ebcd6e97006102cdc8e78a29f19ecdf796a0de", size = 439198, upload-time = "2026-03-02T15:12:39.546Z" }, { url = "https://files.pythonhosted.org/packages/47/d4/2fdf854bc3b9c7f55219678f812600a20a138af2dd847d99004994eada8f/sentry_sdk-2.53.0-py2.py3-none-any.whl", hash = "sha256:46e1ed8d84355ae54406c924f6b290c3d61f4048625989a723fd622aab838899", size = 437908, upload-time = "2026-02-16T11:11:13.227Z" },
] ]
[[package]] [[package]]
@@ -2271,11 +2271,11 @@ wheels = [
[[package]] [[package]]
name = "whitenoise" name = "whitenoise"
version = "6.12.0" version = "6.11.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/2a/55b3f3a4ec326cd077c1c3defeee656b9298372a69229134d930151acd01/whitenoise-6.12.0.tar.gz", hash = "sha256:f723ebb76a112e98816ff80fcea0a6c9b8ecde835f8ddda25df7a30a3c2db6ad", size = 26841, upload-time = "2026-02-27T00:05:42.028Z" } sdist = { url = "https://files.pythonhosted.org/packages/15/95/8c81ec6b6ebcbf8aca2de7603070ccf37dbb873b03f20708e0f7c1664bc6/whitenoise-6.11.0.tar.gz", hash = "sha256:0f5bfce6061ae6611cd9396a8231e088722e4fc67bc13a111be74c738d99375f", size = 26432, upload-time = "2025-09-18T09:16:10.995Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, { url = "https://files.pythonhosted.org/packages/6c/e9/4366332f9295fe0647d7d3251ce18f5615fbcb12d02c79a26f8dba9221b3/whitenoise-6.11.0-py3-none-any.whl", hash = "sha256:b2aeb45950597236f53b5342b3121c5de69c8da0109362aee506ce88e022d258", size = 20197, upload-time = "2025-09-18T09:16:09.754Z" },
] ]
[[package]] [[package]]
-18
View File
@@ -1,18 +0,0 @@
import type * as React from 'react';
declare module '@react-aria/overlays' {
export type PortalProviderContextValue = {
getContainer: () => HTMLElement | null;
};
export type PortalProviderProps = {
getContainer: () => HTMLElement | null;
children: React.ReactNode;
};
export function useUNSAFE_PortalContext(): PortalProviderContextValue;
export function UNSAFE_PortalProvider(
props: PortalProviderProps,
): JSX.Element;
}
+1 -5
View File
@@ -1,7 +1,5 @@
FROM node:20-alpine AS frontend-deps FROM node:20-alpine AS frontend-deps
USER node
WORKDIR /home/frontend/ WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json COPY ./src/frontend/package.json ./package.json
@@ -10,7 +8,7 @@ COPY ./src/frontend/package-lock.json ./package-lock.json
RUN npm ci RUN npm ci
COPY .dockerignore ./.dockerignore COPY .dockerignore ./.dockerignore
COPY --chown=node:node ./src/frontend/ . COPY ./src/frontend/ .
### ---- Front-end builder image ---- ### ---- Front-end builder image ----
FROM frontend-deps AS meet FROM frontend-deps AS meet
@@ -19,8 +17,6 @@ WORKDIR /home/frontend
FROM frontend-deps AS meet-dev FROM frontend-deps AS meet-dev
USER node
WORKDIR /home/frontend WORKDIR /home/frontend
EXPOSE 8080 EXPOSE 8080
+129 -133
View File
@@ -1,12 +1,12 @@
{ {
"name": "meet", "name": "meet",
"version": "1.13.0", "version": "1.10.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "meet", "name": "meet",
"version": "1.13.0", "version": "1.10.0",
"dependencies": { "dependencies": {
"@fontsource-variable/material-symbols-outlined": "5.2.34", "@fontsource-variable/material-symbols-outlined": "5.2.34",
"@fontsource/material-icons-outlined": "5.2.6", "@fontsource/material-icons-outlined": "5.2.6",
@@ -57,7 +57,7 @@
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "5.8.3", "typescript": "5.8.3",
"vite": "7.3.2", "vite": "7.3.1",
"vite-tsconfig-paths": "6.1.1" "vite-tsconfig-paths": "6.1.1"
} }
}, },
@@ -822,9 +822,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", "integrity": "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -6269,13 +6269,10 @@
} }
}, },
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.3.2", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)", "license": "(MPL-2.0 OR Apache-2.0)",
"engines": {
"node": ">=20"
},
"optionalDependencies": { "optionalDependencies": {
"@types/trusted-types": "^2.0.7" "@types/trusted-types": "^2.0.7"
} }
@@ -7161,11 +7158,10 @@
} }
}, },
"node_modules/flatted": { "node_modules/flatted": {
"version": "3.4.2", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
"dev": true, "dev": true
"license": "ISC"
}, },
"node_modules/for-each": { "node_modules/for-each": {
"version": "0.3.3", "version": "0.3.3",
@@ -7632,9 +7628,9 @@
"integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg==" "integrity": "sha512-Rf4YVNYpKjZ6ASAmibcwTNciQ5Co5Ztq6iZPEykHpkoflnD/K5ryE/rHehFsTm4NJj8nKDhbi3eKBWGogmNnkg=="
}, },
"node_modules/hono": { "node_modules/hono": {
"version": "4.12.8", "version": "4.12.2",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.8.tgz", "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.2.tgz",
"integrity": "sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==", "integrity": "sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -11111,9 +11107,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "6.24.1", "version": "6.23.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=18.17" "node": ">=18.17"
@@ -11334,9 +11330,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "7.3.2", "version": "7.3.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -11424,9 +11420,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/aix-ppc64": { "node_modules/vite/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", "integrity": "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -11441,9 +11437,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-arm": { "node_modules/vite/node_modules/@esbuild/android-arm": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.3.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", "integrity": "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -11458,9 +11454,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-arm64": { "node_modules/vite/node_modules/@esbuild/android-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", "integrity": "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11475,9 +11471,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/android-x64": { "node_modules/vite/node_modules/@esbuild/android-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.3.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", "integrity": "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11492,9 +11488,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/darwin-arm64": { "node_modules/vite/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", "integrity": "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11509,9 +11505,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/darwin-x64": { "node_modules/vite/node_modules/@esbuild/darwin-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", "integrity": "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11526,9 +11522,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/freebsd-arm64": { "node_modules/vite/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", "integrity": "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11543,9 +11539,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/freebsd-x64": { "node_modules/vite/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", "integrity": "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11560,9 +11556,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-arm": { "node_modules/vite/node_modules/@esbuild/linux-arm": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", "integrity": "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -11577,9 +11573,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-arm64": { "node_modules/vite/node_modules/@esbuild/linux-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", "integrity": "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11594,9 +11590,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-ia32": { "node_modules/vite/node_modules/@esbuild/linux-ia32": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", "integrity": "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -11611,9 +11607,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-loong64": { "node_modules/vite/node_modules/@esbuild/linux-loong64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", "integrity": "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -11628,9 +11624,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-mips64el": { "node_modules/vite/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", "integrity": "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -11645,9 +11641,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-ppc64": { "node_modules/vite/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", "integrity": "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -11662,9 +11658,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-riscv64": { "node_modules/vite/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", "integrity": "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -11679,9 +11675,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-s390x": { "node_modules/vite/node_modules/@esbuild/linux-s390x": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", "integrity": "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -11696,9 +11692,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/linux-x64": { "node_modules/vite/node_modules/@esbuild/linux-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", "integrity": "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11713,9 +11709,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/netbsd-arm64": { "node_modules/vite/node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", "integrity": "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11730,9 +11726,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/netbsd-x64": { "node_modules/vite/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", "integrity": "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11747,9 +11743,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/openbsd-arm64": { "node_modules/vite/node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", "integrity": "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11764,9 +11760,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/openbsd-x64": { "node_modules/vite/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", "integrity": "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11781,9 +11777,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/sunos-x64": { "node_modules/vite/node_modules/@esbuild/sunos-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", "integrity": "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11798,9 +11794,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-arm64": { "node_modules/vite/node_modules/@esbuild/win32-arm64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", "integrity": "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -11815,9 +11811,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-ia32": { "node_modules/vite/node_modules/@esbuild/win32-ia32": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", "integrity": "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -11832,9 +11828,9 @@
} }
}, },
"node_modules/vite/node_modules/@esbuild/win32-x64": { "node_modules/vite/node_modules/@esbuild/win32-x64": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", "integrity": "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -11849,9 +11845,9 @@
} }
}, },
"node_modules/vite/node_modules/esbuild": { "node_modules/vite/node_modules/esbuild": {
"version": "0.27.7", "version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "integrity": "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -11862,32 +11858,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7", "@esbuild/aix-ppc64": "0.27.3",
"@esbuild/android-arm": "0.27.7", "@esbuild/android-arm": "0.27.3",
"@esbuild/android-arm64": "0.27.7", "@esbuild/android-arm64": "0.27.3",
"@esbuild/android-x64": "0.27.7", "@esbuild/android-x64": "0.27.3",
"@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-arm64": "0.27.3",
"@esbuild/darwin-x64": "0.27.7", "@esbuild/darwin-x64": "0.27.3",
"@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.3",
"@esbuild/freebsd-x64": "0.27.7", "@esbuild/freebsd-x64": "0.27.3",
"@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm": "0.27.3",
"@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-arm64": "0.27.3",
"@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-ia32": "0.27.3",
"@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-loong64": "0.27.3",
"@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-mips64el": "0.27.3",
"@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-ppc64": "0.27.3",
"@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-riscv64": "0.27.3",
"@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-s390x": "0.27.3",
"@esbuild/linux-x64": "0.27.7", "@esbuild/linux-x64": "0.27.3",
"@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.3",
"@esbuild/netbsd-x64": "0.27.7", "@esbuild/netbsd-x64": "0.27.3",
"@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.3",
"@esbuild/openbsd-x64": "0.27.7", "@esbuild/openbsd-x64": "0.27.3",
"@esbuild/openharmony-arm64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.3",
"@esbuild/sunos-x64": "0.27.7", "@esbuild/sunos-x64": "0.27.3",
"@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-arm64": "0.27.3",
"@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-ia32": "0.27.3",
"@esbuild/win32-x64": "0.27.7" "@esbuild/win32-x64": "0.27.3"
} }
}, },
"node_modules/vite/node_modules/fdir": { "node_modules/vite/node_modules/fdir": {
@@ -11909,9 +11905,9 @@
} }
}, },
"node_modules/vite/node_modules/picomatch": { "node_modules/vite/node_modules/picomatch": {
"version": "4.0.4", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"name": "meet", "name": "meet",
"private": true, "private": true,
"version": "1.13.0", "version": "1.10.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "panda codegen && vite", "dev": "panda codegen && vite",
@@ -62,7 +62,7 @@
"postcss": "8.5.6", "postcss": "8.5.6",
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "5.8.3", "typescript": "5.8.3",
"vite": "7.3.2", "vite": "7.3.1",
"vite-tsconfig-paths": "6.1.1" "vite-tsconfig-paths": "6.1.1"
} }
} }
-5
View File
@@ -274,11 +274,6 @@ const config: Config = {
min: { value: 'min-content' }, min: { value: 'min-content' },
max: { value: 'max-content' }, max: { value: 'max-content' },
fit: { value: 'fit-content' }, fit: { value: 'fit-content' },
// room layout
'room-side-panel': { value: '360px' },
'room-side-panel-margin': { value: '1.5rem' },
'room-control-bar': { value: '80px' },
'room-reaction-toolbar-height': { value: '42px' },
}, },
spacing, spacing,
}), }),
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 11 KiB

+1 -13
View File
@@ -15,19 +15,7 @@ export const fetchApi = async <T = Record<string, unknown>>(
...options?.headers, ...options?.headers,
}, },
}) })
const result = await response.json()
let result: T
if (response.status === 204) {
result = undefined as T
} else {
const contentType = response.headers.get('content-type') ?? ''
if (!contentType.includes('application/json')) {
result = undefined as T
} else {
result = (await response.json()) as T
}
}
if (!response.ok) { if (!response.ok) {
throw new ApiError(response.status, result) throw new ApiError(response.status, result)
} }
-1
View File
@@ -5,5 +5,4 @@ export const keys = {
requestEntry: 'requestEntry', requestEntry: 'requestEntry',
waitingParticipants: 'waitingParticipants', waitingParticipants: 'waitingParticipants',
roomCreationCallback: 'roomCreationCallback', roomCreationCallback: 'roomCreationCallback',
files: 'files',
} }
-7
View File
@@ -30,13 +30,6 @@ export interface ApiConfig {
expiration_days?: number expiration_days?: number
max_duration?: number max_duration?: number
} }
background_image: {
upload_is_enabled: boolean
max_size: number
max_count_by_user: number
allowed_extensions: string[]
allowed_mimetypes: string[]
}
subtitle: { subtitle: {
enabled: boolean enabled: boolean
} }
@@ -1,93 +0,0 @@
import { fetchApi } from '@/api/fetchApi'
import { useMutation } from '@tanstack/react-query'
import { ApiFileItem } from '@/features/files/api/types.ts'
import { keys } from '@/api/queryKeys.ts'
import { queryClient } from '@/api/queryClient.ts'
/**
* Upload a file, using XHR so we can report on progress through a handler.
*
* @param url The URL to PUT the file to.
* @param file The file to upload.
* @param progressHandler A handler that receives progress updates as a single integer `0 <= x <= 100`.
*/
export const uploadFile = (
url: string,
file: File,
progressHandler: (progress: number) => void
) =>
new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('PUT', url)
xhr.setRequestHeader('X-amz-acl', 'private')
xhr.setRequestHeader('Content-Type', file.type)
xhr.addEventListener('error', reject)
xhr.addEventListener('abort', reject)
xhr.addEventListener('readystatechange', () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
// Make sure to always set the progress to 100% when the upload is done.
// Because 'progress' event listener is not called when the file size is 0.
progressHandler(100)
return resolve(true)
}
reject(new Error(`Failed to perform the upload on ${url}.`))
}
})
xhr.upload.addEventListener('progress', (progressEvent) => {
if (progressEvent.lengthComputable) {
progressHandler(
Math.floor((progressEvent.loaded / progressEvent.total) * 100)
)
}
})
xhr.send(file)
})
/**
* Asynchronously creates a new file and uploads it to the server.
*
* @param {object} params - The parameters for the file creation and upload process.
* @param {File} params.file - The file object to be uploaded.
* @param {function} params.onProgress - A callback function that receives the upload progress as a number (0 to 100).
* @returns {Promise<ApiFileItem>} A promise that resolves when the file has been successfully uploaded and the server process is completed.
*/
export const createFile = async ({
file,
onProgress,
}: {
file: File
onProgress: (progress: number) => void
}): Promise<ApiFileItem> => {
const res = await fetchApi<ApiFileItem>(`/files/`, {
method: 'POST',
body: JSON.stringify({ filename: file.name, type: 'background_image' }),
})
if (res.upload_state !== 'pending') {
throw new Error('State should be pending right after creation')
}
const policy = res.policy
await uploadFile(policy, file, onProgress)
const createdFile = await fetchApi<ApiFileItem>(
`/files/${res.id}/upload-ended/`,
{
method: 'POST',
}
)
// We invalidate the files query to make sure the new file is immediately available.
await queryClient.invalidateQueries({
queryKey: [keys.files],
})
return createdFile
}
export const useCreateFile = () => {
return useMutation({
mutationFn: createFile,
})
}
@@ -1,33 +0,0 @@
import { fetchApi } from '@/api/fetchApi'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys.ts'
/**
* Deletes a file specified by its unique identifier.
*
* @param {Object} params - The parameters required for deleting the file.
* @param {string} params.fileId - The unique identifier of the file to be deleted.
* @returns {Promise<void>} A promise that resolves when the file is successfully deleted.
*/
export const deleteFile = async ({
fileId,
}: {
fileId: string
}): Promise<void> => {
await fetchApi<void>(`/files/${fileId}/`, {
method: 'DELETE',
})
}
export const useDeleteFile = () => {
const queryClient = useQueryClient()
return useMutation({
mutationFn: deleteFile,
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: [keys.files],
})
},
})
}
@@ -1,70 +0,0 @@
import { fetchApi } from '@/api/fetchApi'
import { keepPreviousData, useQuery } from '@tanstack/react-query'
import { keys } from '@/api/queryKeys'
import {
ApiFileItem,
ApiFileType,
ApiFileUploadState,
} from '@/features/files/api/types.ts'
import { useUser } from '@/features/auth'
import { useConfig } from '@/api/useConfig.ts'
type ListFilesResponse = {
count: number
next: string | null
previous: string | null
results: ApiFileItem[]
}
type ListFilesFilters = {
is_creator_me?: boolean
type?: ApiFileType
upload_state?: ApiFileUploadState
is_deleted?: boolean
}
export type ListFilesParams = {
filters?: ListFilesFilters
pagination: {
page: number
pageSize: number
}
}
export const listMyFiles = async ({
filters = {},
pagination: { page, pageSize },
}: ListFilesParams): Promise<ListFilesResponse> => {
const query = new URLSearchParams()
query.append('page', page.toString())
query.append('page_size', pageSize.toString())
if (filters?.is_creator_me ?? true) {
query.append('is_creator_me', 'true')
}
if (filters?.type) {
query.append('type', filters.type)
}
if (filters?.upload_state) {
query.append('upload_state', filters.upload_state)
}
if (typeof filters?.is_deleted === 'boolean') {
query.append('is_deleted', filters.is_deleted ? 'true' : 'false')
}
return fetchApi<ListFilesResponse>(`/files?${query.toString()}`, {
method: 'GET',
})
}
export const useListMyFiles = (params: Parameters<typeof listMyFiles>[0]) => {
const { isLoggedIn } = useUser()
const { data: appConfig } = useConfig()
return useQuery({
queryKey: [keys.files, params],
queryFn: () => listMyFiles(params),
refetchOnMount: 'always',
placeholderData: keepPreviousData,
enabled:
isLoggedIn && appConfig?.background_image?.upload_is_enabled === true,
})
}
@@ -1,34 +0,0 @@
export type ApiFileCreator = {
id: string // UUID
full_name: string | null
short_name: string | null
}
export type ApiFileType = 'background_image'
export type ApiFileUploadState = 'pending' | 'ready'
export type ApiFileItem = {
id: string // UUID
created_at: string // ISO datetime string
updated_at: string // ISO datetime string
title: string
type: ApiFileType
creator: ApiFileCreator
deleted_at: string | null
hard_deleted_at: string | null
filename: string
upload_state: ApiFileUploadState
mimetype: string // e.g. "image/png"
size: number // file size in bytes
description: string | null
} & (
| {
upload_state: 'ready'
url: string
}
| {
upload_state: 'pending'
policy: string
url: null
}
)
@@ -1,44 +0,0 @@
import { cva, RecipeVariantProps } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
const controlBarRegion = cva({
base: {
display: 'flex',
alignItems: 'center',
flex: '1 1 33%',
justifyContent: 'center',
gap: '0.65rem',
},
variants: {
mobile: {
true: {
justifyContent: 'space-between',
width: '330px',
},
},
},
defaultVariants: {
mobile: false,
},
})
export type ControlBarRegionProps = React.HTMLAttributes<HTMLDivElement> &
RecipeVariantProps<typeof controlBarRegion>
export function ControlBarRegion({
children,
mobile,
...props
}: ControlBarRegionProps) {
const { t } = useTranslation('rooms')
return (
<div
role="region"
aria-label={t('controls.region')}
className={controlBarRegion({ mobile })}
{...props}
>
{children}
</div>
)
}
@@ -1,86 +0,0 @@
// RoomContentArea.tsx
import React from 'react'
import { styled } from '@/styled-system/jsx'
import { cva } from '@/styled-system/css'
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { Subtitles } from '@/features/subtitle/component/Subtitles'
import { MainNotificationToast } from '@/features/notifications/MainNotificationToast'
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
const RoomViewport = styled(
'div',
cva({
base: {
position: 'absolute',
maxHeight: '100%',
transition:
'inset .5s cubic-bezier(0.4,0,0.2,1) 5ms, padding .5s cubic-bezier(0.4,0,0.2,1) 5ms',
},
variants: {
isSidePanelOpen: {
true: {
inset: `var(--lk-grid-gap) calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin) * 2) calc(var(--sizes-room-control-bar)) 16px`,
},
false: {
inset: `var(--lk-grid-gap) var(--lk-grid-gap) calc(var(--sizes-room-control-bar))`,
},
},
isReactionToolbarOpen: {
true: {
paddingBottom:
'calc(var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))',
},
false: {
paddingBottom: '0',
},
},
},
})
)
const TrackAreaContainer = styled(
'div',
cva({
base: {
position: 'relative',
display: 'flex',
width: '100%',
transition: 'height .5s cubic-bezier(0.4,0,0.2,1) 5ms',
},
variants: {
areSubtitlesOpen: {
true: {
height: 'calc(100% - 12rem)',
},
false: {
height: '100%',
},
},
},
})
)
interface RoomContentAreaProps {
children: React.ReactNode
}
export function RoomContentArea({ children }: RoomContentAreaProps) {
const { isSidePanelOpen } = useSidePanel()
const { areSubtitlesOpen } = useSubtitles()
const { isOpen: isReactionToolbarOpen } = useReactionsToolbar()
return (
<RoomViewport
isSidePanelOpen={isSidePanelOpen}
isReactionToolbarOpen={isReactionToolbarOpen}
>
<TrackAreaContainer areSubtitlesOpen={areSubtitlesOpen}>
{children}
</TrackAreaContainer>
<Subtitles />
<MainNotificationToast />
</RoomViewport>
)
}
@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react' import { useEffect, useRef, useState } from 'react'
import { useRoomContext } from '@livekit/components-react' import { useRoomContext } from '@livekit/components-react'
import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client' import { Participant, RemoteParticipant, RoomEvent } from 'livekit-client'
import { ChatMessage, isMobileBrowser } from '@livekit/components-core' import { ChatMessage, isMobileBrowser } from '@livekit/components-core'
@@ -10,15 +10,17 @@ import { decodeNotificationDataReceived } from './utils'
import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification' import { useNotificationSound } from '@/features/notifications/hooks/useSoundNotification'
import { ToastProvider, toastQueue } from './components/ToastProvider' import { ToastProvider, toastQueue } from './components/ToastProvider'
import { WaitingParticipantNotification } from './components/WaitingParticipantNotification' import { WaitingParticipantNotification } from './components/WaitingParticipantNotification'
import {
Emoji,
Reaction,
} from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import {
ANIMATION_DURATION,
ReactionPortals,
} from '@/features/rooms/livekit/components/ReactionPortal'
import { layoutStore } from '@/stores/layout' import { layoutStore } from '@/stores/layout'
import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel' import { PanelId } from '@/features/rooms/livekit/hooks/useSidePanel'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce' import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { Emoji } from '@/features/reactions/types'
import { useReactions } from '@/features/reactions/hooks/useReactions'
// Sliding window of recent chat ids kept for deduplication. Sized to comfortably
// cover bursts and re-emits while staying negligible in memory.
const MAX_TRACKED_CHAT_IDS = 16
export const MainNotificationToast = () => { export const MainNotificationToast = () => {
const room = useRoomContext() const room = useRoomContext()
@@ -26,11 +28,8 @@ export const MainNotificationToast = () => {
const { t } = useTranslation('notifications') const { t } = useTranslation('notifications')
const announce = useScreenReaderAnnounce() const announce = useScreenReaderAnnounce()
const { appendReaction } = useReactions() const [reactions, setReactions] = useState<Reaction[]>([])
const instanceIdRef = useRef(0)
// Multiple Chat instances may re-emit the same RoomEvent.ChatMessage.
// Dedupe against a small ring of recent ids.
const seenChatMsgIdsRef = useRef<string[]>([])
useEffect(() => { useEffect(() => {
const handleChatMessage = ( const handleChatMessage = (
@@ -38,13 +37,6 @@ export const MainNotificationToast = () => {
participant?: Participant | undefined participant?: Participant | undefined
) => { ) => {
if (!participant || participant.isLocal) return if (!participant || participant.isLocal) return
const id = chatMessage.id
if (id) {
const seen = seenChatMsgIdsRef.current
if (seen.includes(id)) return
seen.push(id)
if (seen.length > MAX_TRACKED_CHAT_IDS) seen.shift()
}
triggerNotificationSound(NotificationType.MessageReceived) triggerNotificationSound(NotificationType.MessageReceived)
toastQueue.add( toastQueue.add(
{ {
@@ -70,13 +62,21 @@ export const MainNotificationToast = () => {
} }
}, [room, triggerNotificationSound, announce, t]) }, [room, triggerNotificationSound, announce, t])
const handleEmoji = useCallback( const handleEmoji = (emoji: string, participant: Participant) => {
(emoji: string, participant: Participant) => { if (!emoji || !Object.values(Emoji).includes(emoji as Emoji)) return
if (!emoji || !Object.values(Emoji).includes(emoji as Emoji)) return const id = instanceIdRef.current++
appendReaction(emoji as Emoji, participant) setReactions((prev) => [
}, ...prev,
[appendReaction] {
) id,
emoji,
participant,
},
])
setTimeout(() => {
setReactions((prev) => prev.filter((instance) => instance.id !== id))
}, ANIMATION_DURATION)
}
useEffect(() => { useEffect(() => {
const handleDataReceived = ( const handleDataReceived = (
@@ -149,7 +149,7 @@ export const MainNotificationToast = () => {
return () => { return () => {
room.off(RoomEvent.DataReceived, handleDataReceived) room.off(RoomEvent.DataReceived, handleDataReceived)
} }
}, [room, handleEmoji]) }, [room])
useEffect(() => { useEffect(() => {
const showJoinNotification = (participant: Participant) => { const showJoinNotification = (participant: Participant) => {
@@ -252,6 +252,7 @@ export const MainNotificationToast = () => {
<Div position="absolute" bottom={0} right={5} zIndex={1000}> <Div position="absolute" bottom={0} right={5} zIndex={1000}>
<ToastProvider /> <ToastProvider />
<WaitingParticipantNotification /> <WaitingParticipantNotification />
<ReactionPortals reactions={reactions} />
</Div> </Div>
) )
} }
@@ -1,234 +0,0 @@
import { type ReactNode, useEffect, useMemo, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { useDocumentPiP } from '../hooks/useDocumentPiP'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { useRestoreFocus } from '@/hooks/useRestoreFocus'
import { UNSAFE_PortalProvider } from '@react-aria/overlays'
// Minimal base styles so the PiP window renders correctly on first paint.
const ensureBaseStyles = (target: Document) => {
if (target.getElementById('pip-base-styles')) return
const style = target.createElement('style')
style.id = 'pip-base-styles'
style.textContent = `
html, body { margin: 0; padding: 0; height: 100%; background: #0b0f19; }
body { overflow: hidden; }
* { box-sizing: border-box; }
`
target.head.appendChild(style)
}
// Clone existing styles to keep the PiP window visually consistent.
const copyStyles = (source: Document, target: Document) => {
if (target.getElementById('pip-style-clone')) return
const marker = target.createElement('meta')
marker.id = 'pip-style-clone'
target.head.appendChild(marker)
source.querySelectorAll('style, link[rel="stylesheet"]').forEach((node) => {
const cloned = node.cloneNode(true) as HTMLElement
target.head.appendChild(cloned)
})
}
const syncThemeAttribute = (source: Document, target: Document) => {
const theme = source.documentElement.getAttribute('data-lk-theme')
if (theme) {
target.documentElement.setAttribute('data-lk-theme', theme)
} else {
target.documentElement.removeAttribute('data-lk-theme')
}
}
const cssVarNameCacheByElement = new WeakMap<HTMLElement, string[]>()
const cssVarNameCacheByUri = new Map<string, string[]>()
const syncCssVariables = (source: Document, target: Document) => {
const sourceView = source.defaultView
if (!sourceView) return
const getCachedVarNames = () => {
const docEl = source.documentElement
if (!docEl) return []
const cachedByElement = cssVarNameCacheByElement.get(docEl)
if (cachedByElement) return cachedByElement
const cachedByUri = source.baseURI
? cssVarNameCacheByUri.get(source.baseURI)
: undefined
if (cachedByUri) return cachedByUri
const varNames = new Set<string>()
const collectVarsFrom = (element: HTMLElement | null) => {
if (!element) return
const styles = sourceView.getComputedStyle(element)
for (let i = 0; i < styles.length; i += 1) {
const property = styles[i]
if (property.startsWith('--')) {
varNames.add(property)
}
}
}
collectVarsFrom(source.documentElement)
collectVarsFrom(source.body)
const result = Array.from(varNames)
cssVarNameCacheByElement.set(docEl, result)
if (source.baseURI) {
cssVarNameCacheByUri.set(source.baseURI, result)
}
return result
}
const varNames = getCachedVarNames()
if (!varNames.length) return
const rootStyles = sourceView.getComputedStyle(source.documentElement)
const bodyStyles = source.body
? sourceView.getComputedStyle(source.body)
: null
varNames.forEach((property) => {
const bodyValue = bodyStyles?.getPropertyValue(property)
const value = bodyValue || rootStyles.getPropertyValue(property)
if (value) {
target.documentElement.style.setProperty(property, value)
}
})
}
/**
* React portal into a Document Picture-in-Picture window. Handles window
* lifecycle, style/theme sync and routes React Aria overlays via
* `UNSAFE_PortalProvider` so they render inside the PiP document.
*/
export const DocumentPiPPortal = ({
isOpen,
width,
height,
children,
onClose,
}: {
isOpen: boolean
width?: number
height?: number
children: React.ReactNode
onClose?: () => void
}): ReactNode => {
const { openPiP, closePiP, pipWindow, isSupported } = useDocumentPiP({
width,
height,
})
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
const announce = useScreenReaderAnnounce()
const [container, setContainer] = useState<HTMLElement | null>(null)
const containerRef = useRef<HTMLElement | null>(null)
const prevOpenRef = useRef(false)
useEffect(() => {
if (!isOpen) {
closePiP()
setContainer(null)
containerRef.current = null
return
}
if (!isSupported) return
let cancelled = false
openPiP().then((win) => {
if (!win || cancelled) return
const doc = win.document
ensureBaseStyles(doc)
copyStyles(document, doc)
syncThemeAttribute(document, doc)
syncCssVariables(document, doc)
doc.documentElement.setAttribute('lang', document.documentElement.lang)
doc.title = t('windowLabel')
const existingContainer = containerRef.current
if (!existingContainer || existingContainer.ownerDocument !== doc) {
const nextContainer = doc.createElement('div')
nextContainer.id = 'pip-root'
nextContainer.style.width = '100%'
nextContainer.style.height = '100%'
nextContainer.style.display = 'flex'
nextContainer.style.alignItems = 'stretch'
nextContainer.style.justifyContent = 'center'
doc.body.appendChild(nextContainer)
containerRef.current = nextContainer
setContainer(nextContainer)
} else {
setContainer(existingContainer)
}
})
return () => {
cancelled = true
}
}, [closePiP, isOpen, isSupported, openPiP, t])
// Focus stays on the trigger; PiP is announced as an auxiliary surface.
useEffect(() => {
const wasOpen = prevOpenRef.current
prevOpenRef.current = isOpen
if (isOpen && !wasOpen) {
announce(t('opened'), 'polite')
}
if (!isOpen && wasOpen) {
announce(t('closed'), 'polite')
}
}, [isOpen, announce, t])
useRestoreFocus(isOpen, { restoreFocusRaf: true })
// Escape from either document closes PiP (unless a nested overlay handled it).
useEffect(() => {
if (!isOpen) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape' || event.defaultPrevented) return
event.preventDefault()
onClose?.()
}
document.addEventListener('keydown', handleKeyDown)
pipWindow?.document.addEventListener('keydown', handleKeyDown)
return () => {
document.removeEventListener('keydown', handleKeyDown)
pipWindow?.document.removeEventListener('keydown', handleKeyDown)
}
}, [isOpen, onClose, pipWindow])
useEffect(() => {
if (!pipWindow) return
const handleClose = () => {
containerRef.current = null
setContainer(null)
onClose?.()
}
pipWindow.addEventListener('pagehide', handleClose)
pipWindow.addEventListener('beforeunload', handleClose)
return () => {
pipWindow.removeEventListener('pagehide', handleClose)
pipWindow.removeEventListener('beforeunload', handleClose)
}
}, [onClose, pipWindow])
const portal = useMemo(() => {
if (!container) return null
return createPortal(
<UNSAFE_PortalProvider getContainer={() => container}>
{children}
</UNSAFE_PortalProvider>,
container
)
}, [children, container])
return portal as unknown as ReactNode
}
@@ -1,122 +0,0 @@
import { styled } from '@/styled-system/jsx'
import { useRef, useMemo, useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import { findFirstFocusable } from '@/utils/dom'
import { AudioDevicesControl } from '@/features/rooms/livekit/components/controls/Device/AudioDevicesControl'
import { VideoDeviceControl } from '@/features/rooms/livekit/components/controls/Device/VideoDeviceControl'
import { ScreenShareToggle } from '@/features/rooms/livekit/components/controls/ScreenShareToggle'
import { LeaveButton } from '@/features/rooms/livekit/components/controls/LeaveButton'
import { SubtitlesToggle } from '@/features/rooms/livekit/components/controls/SubtitlesToggle'
import { HandToggle } from '@/features/rooms/livekit/components/controls/HandToggle'
import { StartMediaButton } from '@/features/rooms/livekit/components/controls/StartMediaButton'
import { usePipElementSize } from '../hooks/usePipElementSize'
import { PipOptionsMenu } from './controls/PipOptionsMenu'
import { PipReactionsToggle } from './PipReactionsToggle'
export type CollapsibleControl =
| 'hand'
// | 'subtitles'
| 'screenShare'
| 'reactions'
const COLLAPSE_ORDER: CollapsibleControl[] = [
'hand',
// 'subtitles',
'screenShare',
'reactions',
]
const BUTTON_SLOT = 50
const ESSENTIAL_WIDTH = 260
const getHiddenControls = (
containerWidth: number,
showScreenShare: boolean
): Set<CollapsibleControl> => {
const hidden = new Set<CollapsibleControl>()
if (containerWidth <= 0) return hidden
const collapsible = showScreenShare
? COLLAPSE_ORDER
: COLLAPSE_ORDER.filter((c) => c !== 'screenShare')
const available = containerWidth - ESSENTIAL_WIDTH
const maxVisible = Math.max(0, Math.floor(available / BUTTON_SLOT))
for (let i = 0; i < collapsible.length - maxVisible; i++) {
hidden.add(collapsible[i])
}
return hidden
}
export const PipControlBar = ({
showScreenShare,
}: {
showScreenShare: boolean
}) => {
const containerRef = useRef<HTMLDivElement>(null)
const { width } = usePipElementSize(containerRef)
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
const hidden = useMemo(
() => getHiddenControls(width, showScreenShare),
[width, showScreenShare]
)
useRegisterKeyboardShortcut({
id: 'focus-toolbar',
handler: useCallback(() => {
const doc = containerRef.current?.ownerDocument ?? document
findFirstFocusable(doc.getElementById('pip-control-bar'))?.focus()
}, []),
})
return (
<PipControls
ref={containerRef}
id="pip-control-bar"
role="toolbar"
aria-label={t('controlBar')}
>
<PipControlsCenter>
<AudioDevicesControl hideMenu />
<VideoDeviceControl hideMenu />
{!hidden.has('reactions') && <PipReactionsToggle />}
{showScreenShare && !hidden.has('screenShare') && <ScreenShareToggle />}
{/*{!hidden.has('subtitles') && <SubtitlesToggle />}*/}
{!hidden.has('hand') && <HandToggle />}
<PipOptionsMenu overflowControls={hidden} />
<LeaveButton />
<StartMediaButton />
</PipControlsCenter>
</PipControls>
)
}
const PipControls = styled('div', {
base: {
flex: '0 0 auto',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
gap: '0.5rem',
padding: '0.5rem 0.75rem',
backgroundColor: 'primaryDark.50',
width: '100%',
position: 'relative',
},
})
const PipControlsCenter = styled('div', {
base: {
display: 'flex',
flexWrap: 'nowrap',
justifyContent: 'center',
alignItems: 'center',
gap: '0.4rem',
flex: '1 1 auto',
},
})
@@ -1,34 +0,0 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { RiEmotionLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { useSnapshot } from 'valtio'
import { pipLayoutStore } from '../stores/pipLayoutStore'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
export const PipReactionsToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { showReactionsToolbar: isOpen } = useSnapshot(pipLayoutStore)
const toggle = useCallback(() => {
pipLayoutStore.showReactionsToolbar = !pipLayoutStore.showReactionsToolbar
}, [])
useRegisterKeyboardShortcut({ id: 'reaction', handler: toggle })
return (
<ToggleButton
id="pip-reactions-toggle"
data-attr="pip-reactions-toggle"
square
variant="primaryDark"
aria-label={t('button')}
aria-expanded={isOpen}
tooltip={t('button')}
isSelected={isOpen}
onChange={toggle}
>
<RiEmotionLine />
</ToggleButton>
)
}
@@ -1,69 +0,0 @@
import { useEffect, useRef } from 'react'
import { FocusScope } from '@react-aria/focus'
import { styled } from '@/styled-system/jsx'
import { useSnapshot } from 'valtio'
import { pipLayoutStore } from '../stores/pipLayoutStore'
import { useDelayUnmount } from '@/hooks/useDelayUnmount'
import { usePipElementSize } from '../hooks/usePipElementSize'
import { PipReactionsKeyboardNavigation } from './reactions/PipReactionsKeyboardNavigation'
import { PipReactionsPill } from './reactions/PipReactionsPill'
/**
* Reactions toolbar for the PiP window. Owns only the open/close orchestration;
* layout and pagination live in `PipReactionsPill`, keyboard nav in
* `PipReactionsKeyboardNavigation`.
*/
export const PipReactionsToolbar = () => {
const { showReactionsToolbar: isOpen } = useSnapshot(pipLayoutStore)
// Unmount content after the close transition so hidden emojis leave the tab order.
const renderContent = useDelayUnmount(isOpen, 500)
const contentRef = useRef<HTMLDivElement>(null)
const wrapperRef = useRef<HTMLDivElement>(null)
const { width: availableWidth } = usePipElementSize(wrapperRef)
// Mark the subtree inert during the fade-out so Tab can't land on it.
useEffect(() => {
const el = contentRef.current
if (!el) return
if (isOpen) el.removeAttribute('inert')
else el.setAttribute('inert', '')
}, [isOpen, renderContent])
return (
<Wrapper ref={wrapperRef} isOpen={isOpen}>
{renderContent && (
<div ref={contentRef}>
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
<FocusScope autoFocus>
<PipReactionsKeyboardNavigation>
<PipReactionsPill
isOpen={isOpen}
availableWidth={availableWidth}
/>
</PipReactionsKeyboardNavigation>
</FocusScope>
</div>
)}
</Wrapper>
)
}
const Wrapper = styled('div', {
base: {
display: 'flex',
justifyContent: 'center',
overflow: 'hidden',
maxHeight: 0,
padding: '0 0.5rem',
transition:
'max-height 0.5s cubic-bezier(0.4, 0, 0.2, 1), padding 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
},
variants: {
isOpen: {
true: {
maxHeight: '60px',
padding: '0.5rem 0.5rem 0.25rem',
},
},
},
})
@@ -1,102 +0,0 @@
import { useCallback, useRef } from 'react'
import { supportsScreenSharing } from '@livekit/components-core'
import { useTranslation } from 'react-i18next'
import { styled } from '@/styled-system/jsx'
import { SidePanel } from '@/features/rooms/livekit/components/SidePanel'
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
import { pipLayoutStore } from '../stores/pipLayoutStore'
import { useEscapeDismiss } from '../hooks/useEscapeDismiss'
import { usePipKeyboardShortcuts } from '../hooks/usePipKeyboardShortcuts'
import { usePipRestoreFocus } from '../hooks/usePipRestoreFocus'
import { PipControlBar } from './PipControlBar'
import { PipReactionsToolbar } from './PipReactionsToolbar'
import { PipStage } from './layouts/PipStage'
import { PipNotificationOverlay } from './notifications/PipNotificationOverlay'
import { PipConnectionStateToast } from './notifications/PipConnectionStateToast'
export const PipView = () => {
const browserSupportsScreenSharing = supportsScreenSharing()
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
const containerRef = useRef<HTMLDivElement>(null)
const { isSidePanelOpen, closePanel } = useSidePanel(pipLayoutStore)
// Escape closes the side panel instead of the whole PiP window.
useEscapeDismiss(containerRef, isSidePanelOpen, closePanel)
// Forward keyboard shortcuts (Ctrl+D, Ctrl+E, etc.) to the main store.
usePipKeyboardShortcuts(containerRef)
// Side panels open via a menu item that unmounts on click; fall back to the
// options button so focus returns somewhere visible.
const resolveTrigger = useCallback((activeEl: HTMLElement | null) => {
if (activeEl?.tagName === 'DIV') {
const doc = containerRef.current?.ownerDocument ?? document
return doc.getElementById('room-options-trigger')
}
return activeEl
}, [])
usePipRestoreFocus(containerRef, isSidePanelOpen, { resolveTrigger })
return (
<PipContainer
ref={containerRef}
role="region"
aria-label={t('windowLabel')}
>
<PipStage />
<PipReactionsToolbar />
<PipControlBar showScreenShare={browserSupportsScreenSharing} />
<SidePanel store={pipLayoutStore} />
<OverlayStack>
<PipConnectionStateToast />
<PipNotificationOverlay />
</OverlayStack>
</PipContainer>
)
}
const OverlayStack = styled('div', {
base: {
position: 'absolute',
top: '0.5rem',
left: '0.5rem',
right: '0.5rem',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '0.375rem',
pointerEvents: 'none',
zIndex: 1000,
'& > *': { pointerEvents: 'auto' },
},
})
const PipContainer = styled('div', {
base: {
position: 'relative',
width: '100%',
height: '100%',
display: 'grid',
gridTemplateRows: 'minmax(0, 1fr) auto auto',
backgroundColor: 'primaryDark.50',
// Disable LiveKit's own border-radius on tiles so our containers
// (GridCell, Thumbnail, StageFrame) own the clipping exclusively.
'--lk-border-radius': '4px',
'& .lk-participant-tile': {
height: '100%',
},
'& .lk-participant-media': {
height: '100%',
},
'& .lk-participant-media-video': {
height: '100%',
objectFit: 'cover',
},
'& .lk-grid-layout': {
height: '100%',
width: '100%',
},
},
})
@@ -1,30 +0,0 @@
import { useEffect, type ReactNode } from 'react'
import { roomPiPStore } from '@/stores/roomPiP'
import { DocumentPiPPortal } from './DocumentPiPPortal'
import { PipView } from './PipView'
import { useRoomPiP } from '../hooks/useRoomPiP'
/**
* Wrapper that mounts the PiP UI when room-level PiP state is enabled.
* Bridges Valtio-backed PiP state with DocumentPiPPortal and PipView rendering.
* PiP panel state is decoupled via explicit pipLayoutStore injection.
*/
export const RoomPiP = (): ReactNode => {
const { isOpen, close } = useRoomPiP()
// Reset PiP state on unmount (e.g. leaving the room) so the next session
// starts with PiP closed and doesn't try to auto-reopen without a user gesture.
useEffect(() => {
return () => {
roomPiPStore.isOpen = false
}
}, [])
const portal = DocumentPiPPortal({
isOpen,
onClose: close,
children: <PipView />,
})
return portal as ReactNode
}
@@ -1,105 +0,0 @@
import { useEffect, useRef, useState } from 'react'
import { RiMoreFill } from '@remixicon/react'
import { FocusScope } from '@react-aria/focus'
import { Box, Button } from '@/primitives'
import { css } from '@/styled-system/css'
import { useTranslation } from 'react-i18next'
import { PipOptionsMenuItems } from './PipOptionsMenuItems'
import { useEscapeDismiss } from '@/features/pip/hooks/useEscapeDismiss'
import type { CollapsibleControl } from '../PipControlBar'
type PipOptionsMenuProps = {
overflowControls?: Set<CollapsibleControl>
}
/**
* PiP-native options menu. The shared `Menu` primitive mis-positions its
* popover and loses focus across documents, so we drive open/close, focus
* and dismissal ourselves.
*/
export const PipOptionsMenu = ({ overflowControls }: PipOptionsMenuProps) => {
const { t } = useTranslation('rooms')
const wrapperRef = useRef<HTMLDivElement>(null)
const triggerRef = useRef<HTMLButtonElement>(null)
const [isOpen, setIsOpen] = useState(false)
const label = t('options.buttonLabel')
useEscapeDismiss(wrapperRef, isOpen, () => {
setIsOpen(false)
requestAnimationFrame(() => triggerRef.current?.focus())
})
useEffect(() => {
if (!isOpen) return
const doc = wrapperRef.current?.ownerDocument ?? document
const handleMenuItemClick = (event: MouseEvent) => {
const target = event.target as HTMLElement | null
const wrapper = wrapperRef.current
if (!wrapper || !target) return
if (wrapper.querySelector('button')?.contains(target)) return
if (target.closest('[role="menuitem"]')) {
requestAnimationFrame(() => {
setIsOpen(false)
triggerRef.current?.focus()
})
}
}
const handleOutsideClick = (event: MouseEvent) => {
const target = event.target as HTMLElement | null
const wrapper = wrapperRef.current
if (!wrapper || !target) return
if (wrapper.contains(target)) return
setIsOpen(false)
}
doc.addEventListener('click', handleMenuItemClick, true)
doc.addEventListener('mousedown', handleOutsideClick, true)
return () => {
doc.removeEventListener('click', handleMenuItemClick, true)
doc.removeEventListener('mousedown', handleOutsideClick, true)
}
}, [isOpen])
return (
<div
ref={wrapperRef}
className={css({
position: 'relative',
})}
>
<Button
ref={triggerRef}
id="room-options-trigger"
square
variant="primaryDark"
aria-label={label}
aria-haspopup="menu"
aria-expanded={isOpen}
tooltip={label}
onPress={() => setIsOpen(!isOpen)}
>
<RiMoreFill />
</Button>
{isOpen && (
<div
className={css({
position: 'absolute',
left: '50%',
bottom: 'calc(100% + 0.85rem)',
transform: 'translateX(-50%)',
zIndex: 10,
})}
>
{/* eslint-disable-next-line jsx-a11y/no-autofocus */}
<FocusScope autoFocus>
<Box size="sm" type="popover" variant="dark">
<PipOptionsMenuItems overflowControls={overflowControls} />
</Box>
</FocusScope>
</div>
)}
</div>
)
}
@@ -1,48 +0,0 @@
import { Menu as RACMenu, MenuSection } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { Separator } from '@/primitives/Separator'
import { FeedbackMenuItem } from '@/features/rooms/livekit/components/controls/Options/FeedbackMenuItem'
import { EffectsMenuItem } from '@/features/rooms/livekit/components/controls/Options/EffectsMenuItem'
import { SupportMenuItem } from '@/features/rooms/livekit/components/controls/Options/SupportMenuItem'
import { PictureInPictureMenuItem } from '@/features/rooms/livekit/components/controls/Options/PictureInPictureMenuItem'
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
import { PipOverflowItems } from './PipOverflowItems'
import type { CollapsibleControl } from '../PipControlBar'
type PipOptionsMenuItemsProps = {
overflowControls?: Set<CollapsibleControl>
}
export const PipOptionsMenuItems = ({
overflowControls,
}: PipOptionsMenuItemsProps) => {
const { t } = useTranslation('rooms')
const hasOverflow = overflowControls && overflowControls.size > 0
return (
<RACMenu
style={{
minWidth: '150px',
width: '300px',
}}
>
{hasOverflow && (
<>
<MenuSection>
<PipOverflowItems overflowControls={overflowControls} t={t} />
</MenuSection>
<Separator />
</>
)}
<MenuSection>
<PictureInPictureMenuItem />
<EffectsMenuItem store={pipLayoutStore} />
</MenuSection>
<Separator />
{/*<MenuSection>*/}
{/* <SupportMenuItem />*/}
{/* <FeedbackMenuItem />*/}
{/*</MenuSection>*/}
</RACMenu>
)
}
@@ -1,74 +0,0 @@
import { MenuItem } from 'react-aria-components'
import {
RiHand,
RiClosedCaptioningLine,
RiArrowUpLine,
RiEmotionLine,
} from '@remixicon/react'
import { TFunction } from 'i18next'
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
import { menuRecipe } from '@/primitives/menuRecipe'
import { useRoomContext } from '@livekit/components-react'
import { useRaisedHand } from '@/features/rooms/livekit/hooks/useRaisedHand'
import { useSubtitles } from '@/features/subtitle/hooks/useSubtitles'
import { useAreSubtitlesAvailable } from '@/features/subtitle/hooks/useAreSubtitlesAvailable'
import { useSnapshot } from 'valtio'
import type { CollapsibleControl } from '../PipControlBar'
type PipOverflowItemsProps = {
overflowControls: Set<CollapsibleControl>
t: TFunction<'rooms'>
}
export const PipOverflowItems = ({
overflowControls,
t,
}: PipOverflowItemsProps) => {
const room = useRoomContext()
const { isHandRaised, toggleRaisedHand } = useRaisedHand({
participant: room.localParticipant,
})
const { areSubtitlesOpen, toggleSubtitles } = useSubtitles()
const areSubtitlesAvailable = useAreSubtitlesAvailable()
const pipSnap = useSnapshot(pipLayoutStore)
const toggleReactions = () => {
pipLayoutStore.showReactionsToolbar = !pipSnap.showReactionsToolbar
}
const itemClass = menuRecipe({ icon: true, variant: 'dark' }).item
return (
<>
{overflowControls.has('reactions') && (
<MenuItem onAction={toggleReactions} className={itemClass}>
<RiEmotionLine size={20} />
{t('controls.reactions.button')}
</MenuItem>
)}
{overflowControls.has('screenShare') && (
<MenuItem
onAction={() => {
/* screen share requires track toggle, handled externally */
}}
className={itemClass}
>
<RiArrowUpLine size={20} />
{t('controls.screenShare.start')}
</MenuItem>
)}
{/*{overflowControls.has('subtitles') && areSubtitlesAvailable && (*/}
{/* <MenuItem onAction={toggleSubtitles} className={itemClass}>*/}
{/* <RiClosedCaptioningLine size={20} />*/}
{/* {areSubtitlesOpen*/}
{/* ? t('controls.subtitles.open')*/}
{/* : t('controls.subtitles.closed')}*/}
{/* </MenuItem>*/}
{/*)}*/}
{overflowControls.has('hand') && (
<MenuItem onAction={toggleRaisedHand} className={itemClass}>
<RiHand size={20} />
{isHandRaised ? t('controls.hand.lower') : t('controls.hand.raise')}
</MenuItem>
)}
</>
)
}
@@ -1,81 +0,0 @@
import { memo } from 'react'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { styled } from '@/styled-system/jsx'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { getTrackKey } from '../../utils/pipTrackSelection'
type PipFocusLayoutProps = {
mainTrack: TrackReferenceOrPlaceholder
thumbnailTrack?: TrackReferenceOrPlaceholder
}
/**
* Focus layout used when 1-2 tracks are visible in the PiP window.
*
* The main tile is letterboxed (object-fit: contain) so the camera is
* never stretched to a non-video aspect and leaves dark padding
* above/below when the window shape doesn't match the source.
* The thumbnail keeps the usual cover fill.
*/
export const PipFocusLayout = memo(
({ mainTrack, thumbnailTrack }: PipFocusLayoutProps) => {
return (
<FocusContainer>
<MainSlot>
<ParticipantTile
key={getTrackKey(mainTrack)}
trackRef={mainTrack}
disableMetadata
/>
</MainSlot>
{thumbnailTrack && (
<Thumbnail>
<ParticipantTile
key={getTrackKey(thumbnailTrack)}
trackRef={thumbnailTrack}
disableMetadata
/>
</Thumbnail>
)}
</FocusContainer>
)
}
)
PipFocusLayout.displayName = 'PipFocusLayout'
const FocusContainer = styled('div', {
base: {
position: 'relative',
width: '100%',
height: '100%',
borderRadius: '4px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
},
})
const MainSlot = styled('div', {
base: {
width: '100%',
height: '100%',
'& .lk-participant-media-video': {
objectFit: 'contain',
},
},
})
const Thumbnail = styled('div', {
base: {
position: 'absolute',
right: '1rem',
bottom: '1rem',
width: '42%',
maxWidth: '220px',
minWidth: '140px',
aspectRatio: '16 / 9',
borderRadius: '4px',
overflow: 'hidden',
boxShadow: 'md',
zIndex: 2,
},
})
@@ -1,81 +0,0 @@
import { memo, useMemo, useRef } from 'react'
import type { TrackReferenceOrPlaceholder } from '@livekit/components-core'
import { styled } from '@/styled-system/jsx'
import { ParticipantTile } from '@/features/rooms/livekit/components/ParticipantTile'
import { usePipElementSize } from '../../hooks/usePipElementSize'
import { usePipFlipAnimations } from '../../hooks/usePipFlipAnimations'
import { computePipGridLayout } from '../../utils/pipGrid'
import { getTrackKey } from '../../utils/pipTrackSelection'
type PipGridLayoutProps = {
tracks: TrackReferenceOrPlaceholder[]
}
/**
* Adaptive grid used when 3+ tracks are visible in the PiP window.
*
* All grid math (shape choice + partial-row stretching) is delegated to
* `computePipGridLayout`. This component only measures the container,
* applies the returned placements, and plays a FLIP animation when the
* tile set or grid shape changes (participant joins/leaves or shape shift).
*
* Tiles keep a stable key so resizing never remounts <video> elements.
*/
export const PipGridLayout = memo(({ tracks }: PipGridLayoutProps) => {
const containerRef = useRef<HTMLDivElement>(null)
const { width, height } = usePipElementSize(containerRef)
const tileKeys = useMemo(() => tracks.map(getTrackKey), [tracks])
const { rows, subColumns, placements } = useMemo(
() => computePipGridLayout(tracks.length, width, height),
[tracks.length, width, height]
)
const gridStyle = useMemo(
() => ({
gridTemplateColumns: `repeat(${subColumns}, minmax(0, 1fr))`,
gridTemplateRows: `repeat(${rows}, minmax(0, 1fr))`,
}),
[subColumns, rows]
)
usePipFlipAnimations(containerRef, tileKeys)
return (
<GridContainer ref={containerRef} style={gridStyle}>
{tracks.map((track, index) => (
<GridCell key={tileKeys[index]} style={placements[index]}>
<ParticipantTile trackRef={track} disableMetadata />
</GridCell>
))}
</GridContainer>
)
})
PipGridLayout.displayName = 'PipGridLayout'
const GridContainer = styled('div', {
base: {
width: '100%',
height: '100%',
display: 'grid',
gap: '0.25rem',
},
})
const GridCell = styled('div', {
base: {
position: 'relative',
minWidth: 0,
minHeight: 0,
borderRadius: '4px',
overflow: 'hidden',
backgroundColor: 'primaryDark.100',
// Paint on own layer so FLIP transforms don't trigger layout thrash.
willChange: 'transform',
'& .lk-participant-tile': {
width: '100%',
height: '100%',
},
},
})
@@ -1,87 +0,0 @@
import { useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { useTracks } from '@livekit/components-react'
import { Track } from 'livekit-client'
import { styled } from '@/styled-system/jsx'
import {
isCameraTrack,
pickLocalCameraTrack,
pickRemoteCameraTrack,
pickScreenShareTrack,
} from '../../utils/pipTrackSelection'
import { PipFocusLayout } from './PipFocusLayout'
import { PipGridLayout } from './PipGridLayout'
/**
* Above this count the PiP stage switches from the focus layout
* (main + thumbnail) to the adaptive grid layout.
*/
const FOCUS_MAX_TILES = 2
// Handles which layout to render inside the PiP stage.
export const PipStage = () => {
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
{ source: Track.Source.ScreenShare, withPlaceholder: false },
],
{ onlySubscribed: false }
)
const screenShareTrack = useMemo(() => pickScreenShareTrack(tracks), [tracks])
// Order the list so the "focus target" (screen share when available,
// otherwise a remote camera) is first. Both layouts consume this order.
const stageTracks = useMemo(() => {
const cameraTracks = tracks.filter(isCameraTrack)
if (!screenShareTrack) return cameraTracks
return [screenShareTrack, ...cameraTracks]
}, [tracks, screenShareTrack])
// avoid tabbing to the stage when it's not visible
const frameRef = useRef<HTMLDivElement>(null)
useEffect(() => {
frameRef.current?.setAttribute('inert', '')
}, [])
if (stageTracks.length === 0) return null
const stageLabel = t('stage')
if (stageTracks.length > FOCUS_MAX_TILES) {
return (
<StageFrame ref={frameRef} role="region" aria-label={stageLabel}>
<PipGridLayout tracks={stageTracks} />
</StageFrame>
)
}
const localCameraTrack = pickLocalCameraTrack(stageTracks)
const remoteCameraTrack = pickRemoteCameraTrack(stageTracks)
const mainTrack = screenShareTrack ?? remoteCameraTrack ?? stageTracks[0]
const thumbnailTrack =
localCameraTrack && localCameraTrack !== mainTrack
? localCameraTrack
: stageTracks.find((track) => track !== mainTrack)
return (
<StageFrame ref={frameRef} role="region" aria-label={stageLabel}>
<PipFocusLayout mainTrack={mainTrack} thumbnailTrack={thumbnailTrack} />
</StageFrame>
)
}
const StageFrame = styled('div', {
base: {
position: 'relative',
minWidth: 0,
minHeight: 0,
margin: '0.5rem',
borderRadius: '4px',
overflow: 'hidden',
},
})
@@ -1,45 +0,0 @@
import { useConnectionState, useRoomContext } from '@livekit/components-react'
import { ConnectionState } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { styled } from '@/styled-system/jsx'
/**
* Banner surfaced inside the PiP when the room connection degrades.
*
* Scoped to `Reconnecting` / `Disconnected` - the two states the user needs
* to see while their attention is on the PiP rather than the main window.
*/
export const PipConnectionStateToast = () => {
const room = useRoomContext()
const state = useConnectionState(room)
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture.connection',
})
const connectionLabels: Partial<Record<ConnectionState, string>> = {
[ConnectionState.Reconnecting]: t('reconnecting'),
[ConnectionState.Disconnected]: t('disconnected'),
}
const label = connectionLabels[state] ?? null
if (!label) return null
return <Banner role="status">{label}</Banner>
}
const Banner = styled('div', {
base: {
backgroundColor: 'greyscale.800',
color: 'white',
fontSize: '0.8125rem',
lineHeight: 1.3,
padding: '0.375rem 0.75rem',
borderRadius: '6px',
boxShadow:
'rgba(0, 0, 0, 0.4) 0px 2px 6px 0px, rgba(0, 0, 0, 0.25) 0px 4px 12px 2px',
animation: 'fade 200ms',
'@media (prefers-reduced-motion: reduce)': {
animation: 'none',
},
},
})
@@ -1,79 +0,0 @@
import { useToastQueue } from '@react-stately/toast'
import { RiCloseLine } from '@remixicon/react'
import { styled } from '@/styled-system/jsx'
import { Button } from '@/primitives'
import { useTranslation } from 'react-i18next'
import {
toastQueue,
type ToastData,
} from '@/features/notifications/components/ToastProvider'
import { PipToastBody } from './PipToastBody'
/**
* Shows shared toasts in the PiP window.
* We use a local aria-live region so screen readers can read them in PiP.
*/
const MAX_VISIBLE = 3
export const PipNotificationOverlay = () => {
const state = useToastQueue<ToastData>(toastQueue)
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
if (state.visibleToasts.length === 0) return null
const toasts = state.visibleToasts.slice(0, MAX_VISIBLE)
return (
<Region
role="region"
aria-label={t('notificationsLabel')}
aria-live="polite"
>
{toasts.map((toast) => (
<ToastCard key={toast.key} aria-atomic="true">
<PipToastBody toast={toast} />
<Button
square
size="sm"
invisible
aria-label={t('dismissNotification')}
onPress={() => state.close(toast.key)}
>
<RiCloseLine size={16} color="white" aria-hidden="true" />
</Button>
</ToastCard>
))}
</Region>
)
}
const Region = styled('div', {
base: {
display: 'flex',
flexDirection: 'column',
gap: '0.375rem',
alignItems: 'center',
width: '100%',
},
})
const ToastCard = styled('div', {
base: {
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
maxWidth: '100%',
backgroundColor: 'greyscale.700',
color: 'white',
borderRadius: '6px',
boxShadow:
'rgba(0, 0, 0, 0.4) 0px 2px 6px 0px, rgba(0, 0, 0, 0.25) 0px 4px 12px 2px',
paddingRight: '0.25rem',
animation: 'fade 200ms',
'@media (prefers-reduced-motion: reduce)': {
animation: 'none',
},
},
})
@@ -1,119 +0,0 @@
import type { QueuedToast } from '@react-stately/toast'
import { useTranslation } from 'react-i18next'
import { RiHand, RiMessage2Line } from '@remixicon/react'
import type { ReactNode } from 'react'
import { css } from '@/styled-system/css'
import { HStack } from '@/styled-system/jsx'
import { NotificationType } from '@/features/notifications/NotificationType'
import type { ToastData } from '@/features/notifications/components/ToastProvider'
import { RecordingMode } from '@/features/recording'
type Props = {
toast: QueuedToast<ToastData>
}
/**
* Renders the toast content used in PiP.
* PiP stays display-only, so main-window actions are not shown here.
*/
export const PipToastBody = ({ toast }: Props) => {
const { t } = useTranslation('notifications')
const { type, participant, message, removedSources } = toast.content
const name = participant?.name || t('defaultName')
switch (type) {
case NotificationType.ParticipantJoined:
return <Line>{t('joined.description', { name })}</Line>
case NotificationType.ParticipantMuted:
return <Line>{t('muted', { name })}</Line>
case NotificationType.HandRaised:
return (
<Line>
<RiHand
size={16}
color="white"
className={iconStyle}
aria-hidden="true"
/>
{t('raised.description', { name })}
</Line>
)
case NotificationType.MessageReceived:
return (
<Line>
<RiMessage2Line
size={16}
color="white"
className={iconStyle}
aria-hidden="true"
/>
<span>
<strong>{name}</strong>
{message ? ` - ${message}` : null}
</span>
</Line>
)
case NotificationType.TranscriptionStarted:
return <Line>{t('transcript.started', { name })}</Line>
case NotificationType.TranscriptionStopped:
return <Line>{t('transcript.stopped', { name })}</Line>
case NotificationType.TranscriptionLimitReached:
return <Line>{t('transcript.limitReached')}</Line>
case NotificationType.TranscriptionRequested:
return <Line>{t('transcript.requested', { name })}</Line>
case NotificationType.ScreenRecordingStarted:
return <Line>{t('screenRecording.started', { name })}</Line>
case NotificationType.ScreenRecordingStopped:
return <Line>{t('screenRecording.stopped', { name })}</Line>
case NotificationType.ScreenRecordingLimitReached:
return <Line>{t('screenRecording.limitReached')}</Line>
case NotificationType.ScreenRecordingRequested:
return <Line>{t('screenRecording.requested', { name })}</Line>
case NotificationType.RecordingSaving: {
const mode = toast.content.mode as RecordingMode | undefined
const key =
mode === RecordingMode.ScreenRecording
? 'recordingSave.screenRecording.default'
: 'recordingSave.transcript.default'
return <Line>{t(key)}</Line>
}
case NotificationType.PermissionsRemoved: {
const key = resolvePermissionsKey(removedSources)
if (!key) return null
return <Line>{t(`permissionsRemoved.${key}`)}</Line>
}
default:
return message ? <Line>{message}</Line> : null
}
}
const resolvePermissionsKey = (sources: unknown): string | null => {
if (!Array.isArray(sources) || sources.length === 0) return null
if (sources.length === 1) return sources[0] as string
if (sources.includes('screen_share')) return 'screen_share'
return null
}
const Line = ({ children }: { children: ReactNode }) => (
<HStack
alignItems="center"
gap="0.5rem"
padding="0.625rem 0.75rem"
className={css({
fontSize: '0.8125rem',
lineHeight: 1.3,
})}
>
{children}
</HStack>
)
const iconStyle = css({ flexShrink: 0 })
@@ -1,65 +0,0 @@
import { useRef, type ReactNode } from 'react'
import { useTranslation } from 'react-i18next'
import { useFocusManager } from '@react-aria/focus'
import { findFirstFocusable } from '@/utils/dom'
import { pipLayoutStore } from '@/features/pip/stores/pipLayoutStore'
import { useEscapeDismiss } from '@/features/pip/hooks/useEscapeDismiss'
const REACTIONS_TOGGLE_ID = 'pip-reactions-toggle'
const CONTROL_BAR_ID = 'pip-control-bar'
const closeToolbar = () => {
pipLayoutStore.showReactionsToolbar = false
}
/** Keyboard navigation for the PiP reactions toolbar (mirrors the main app). */
export const PipReactionsKeyboardNavigation = ({
children,
}: {
children: ReactNode
}) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const focusManager = useFocusManager()
const rootRef = useRef<HTMLDivElement>(null)
useEscapeDismiss(rootRef, true, () => {
const doc = rootRef.current?.ownerDocument ?? document
doc.getElementById(REACTIONS_TOGGLE_ID)?.focus()
closeToolbar()
})
const onFocus = (event: React.FocusEvent<HTMLDivElement>) => {
const fromOutside = !event.currentTarget.contains(event.relatedTarget)
if (fromOutside) focusManager?.focusFirst()
}
const onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
switch (event.key) {
case 'ArrowRight':
focusManager?.focusNext({ wrap: true })
break
case 'ArrowLeft':
focusManager?.focusPrevious({ wrap: true })
break
case 'Tab':
if (!event.shiftKey) {
event.preventDefault()
const doc = rootRef.current?.ownerDocument ?? document
findFirstFocusable(doc.getElementById(CONTROL_BAR_ID))?.focus()
}
break
}
}
return (
<div
ref={rootRef}
role="toolbar"
aria-label={t('toolbar')}
onFocus={onFocus}
onKeyDown={onKeyDown}
>
{children}
</div>
)
}
@@ -1,160 +0,0 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { styled } from '@/styled-system/jsx'
import { ReactionButton } from '@/features/reactions/components/toolbar/ReactionButton'
import {
computeReactionsPage,
getMaxPageStart,
} from '../../utils/pipReactionsPagination'
type Props = { isOpen: boolean; availableWidth: number }
/**
* Paginated emoji pill with animated entry/exit. Responsibility: layout the
* visible emojis for the currently available width and expose prev/next arrows.
*/
export const PipReactionsPill = ({ isOpen, availableWidth }: Props) => {
const { t } = useTranslation('rooms', {
keyPrefix: 'options.items.pictureInPicture',
})
const [isVisible, setIsVisible] = useState(false)
const [pageStart, setPageStart] = useState(0)
useEffect(() => {
if (!isOpen) {
setIsVisible(false)
return
}
const id = requestAnimationFrame(() => setIsVisible(true))
return () => cancelAnimationFrame(id)
}, [isOpen])
const { visibleEmojis, hasOverflow, canGoLeft, canGoRight, visibleCount } =
useMemo(
() => computeReactionsPage(availableWidth, pageStart),
[availableWidth, pageStart]
)
// Clamp pageStart if the window was resized and the current page no longer fits.
useEffect(() => {
if (!hasOverflow) {
setPageStart(0)
return
}
const maxStart = getMaxPageStart(visibleCount)
if (pageStart > maxStart) setPageStart(maxStart)
}, [hasOverflow, pageStart, visibleCount])
const paginate = useCallback((direction: 'left' | 'right') => {
setPageStart((current) =>
direction === 'left' ? Math.max(0, current - 1) : current + 1
)
}, [])
return (
<Pill isVisible={isVisible}>
{hasOverflow && (
<ArrowSlot>
{canGoLeft && (
<ArrowButton
type="button"
onClick={() => paginate('left')}
aria-label={t('previousReactions')}
>
<RiArrowLeftSLine size={16} />
</ArrowButton>
)}
</ArrowSlot>
)}
<EmojiRow>
{visibleEmojis.map((emoji) => (
<ReactionButton key={emoji} emoji={emoji} />
))}
</EmojiRow>
{hasOverflow && (
<ArrowSlot>
{canGoRight && (
<ArrowButton
type="button"
onClick={() => paginate('right')}
aria-label={t('nextReactions')}
>
<RiArrowRightSLine size={16} />
</ArrowButton>
)}
</ArrowSlot>
)}
</Pill>
)
}
const Pill = styled('div', {
base: {
display: 'flex',
alignItems: 'center',
gap: '0.2rem',
borderRadius: '21px',
padding: '0.15rem',
backgroundColor: 'primaryDark.100',
maxWidth: '100%',
overflow: 'hidden',
width: 'fit-content',
opacity: 0,
transform: 'translateY(3.25rem)',
transition: 'opacity, transform',
transitionDuration: '0.5s',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
pointerEvents: 'none',
},
variants: {
isVisible: {
true: {
opacity: 1,
transform: 'translateY(0)',
pointerEvents: 'auto',
},
},
},
})
const EmojiRow = styled('div', {
base: {
display: 'flex',
gap: '0.2rem',
'& > *': {
flexShrink: 0,
},
},
})
const ArrowSlot = styled('div', {
base: {
width: '32px',
minWidth: '32px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
},
})
const ArrowButton = styled('button', {
base: {
flexShrink: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '28px',
height: '28px',
borderRadius: '50%',
border: 'none',
backgroundColor: 'primaryDark.200',
color: 'white',
cursor: 'pointer',
opacity: 0.85,
_hover: {
opacity: 1,
backgroundColor: 'primaryDark.300',
},
},
})
@@ -1,107 +0,0 @@
import { useCallback, useEffect, useRef, useState } from 'react'
type DocumentPictureInPicture = {
requestWindow: (options?: {
width?: number
height?: number
}) => Promise<Window>
}
type WindowWithDocumentPiP = Window & {
documentPictureInPicture?: DocumentPictureInPicture
}
export const useDocumentPiP = ({
width = 400,
height = 480,
}: {
width?: number
height?: number
} = {}) => {
const [pipWindow, setPipWindow] = useState<Window | null>(null)
const pipWindowRef = useRef<Window | null>(null)
const pendingPiPRef = useRef<Promise<Window | null> | null>(null)
const [isSupported] = useState(() => {
if (typeof globalThis === 'undefined') return false
return 'documentPictureInPicture' in globalThis
})
const openPiP = useCallback(async () => {
if (!isSupported) return null
const existingWindow = pipWindowRef.current
if (existingWindow && !existingWindow.closed) return existingWindow
if (pendingPiPRef.current) return pendingPiPRef.current
// Request a new PiP window from the browser API.
const pip = (globalThis as unknown as WindowWithDocumentPiP)
.documentPictureInPicture
if (!pip) return null
const requestPromise = (async () => {
try {
const win = await pip.requestWindow({ width, height })
const currentWindow = pipWindowRef.current
if (currentWindow && !currentWindow.closed) return currentWindow
setPipWindow(win)
return win
} catch (error) {
// Avoid unhandled rejections if the user blocks or closes the request.
console.error('Failed to open Picture-in-Picture window', error)
return null
} finally {
pendingPiPRef.current = null
}
})()
pendingPiPRef.current = requestPromise
return requestPromise
}, [height, isSupported, width])
const closePiP = useCallback(() => {
if (!pipWindow) return
if (!pipWindow.closed) {
pipWindow.close()
}
setPipWindow(null)
}, [pipWindow])
useEffect(() => {
pipWindowRef.current = pipWindow
}, [pipWindow])
// Force-close the native PiP window when the hook unmounts (e.g. the user
// hangs up and the room is navigated away before `closePiP` could run).
useEffect(() => {
return () => {
const win = pipWindowRef.current
if (win && !win.closed) win.close()
pipWindowRef.current = null
}
}, [])
useEffect(() => {
if (!pipWindow) return
const handleClose = () => {
setPipWindow(null)
}
pipWindow.addEventListener('pagehide', handleClose)
pipWindow.addEventListener('beforeunload', handleClose)
return () => {
pipWindow.removeEventListener('pagehide', handleClose)
pipWindow.removeEventListener('beforeunload', handleClose)
}
}, [pipWindow])
return {
isSupported,
isOpen: !!pipWindow && !pipWindow.closed,
pipWindow,
openPiP,
closePiP,
}
}
@@ -1,28 +0,0 @@
import { useEffect, useRef, type RefObject } from 'react'
export const useEscapeDismiss = (
ref: RefObject<HTMLElement | null>,
isActive: boolean,
onDismiss: () => void
) => {
const latestOnDismiss = useRef(onDismiss)
useEffect(() => {
latestOnDismiss.current = onDismiss
})
useEffect(() => {
if (!isActive) return
const el = ref.current
if (!el) return
const handler = (event: KeyboardEvent) => {
if (event.key !== 'Escape' || event.defaultPrevented) return
event.preventDefault()
event.stopPropagation()
latestOnDismiss.current()
}
el.addEventListener('keydown', handler)
return () => el.removeEventListener('keydown', handler)
}, [ref, isActive])
}
@@ -1,42 +0,0 @@
import { useCallback, useEffect, useState, type RefObject } from 'react'
type Size = { width: number; height: number }
/**
* Observes an element's size, even when mounted in the PiP document.
* Resolves `ResizeObserver` from the element's own window.
*/
export const usePipElementSize = <T extends HTMLElement>(
ref: RefObject<T | null>
): Size => {
const [size, setSize] = useState<Size>({ width: 0, height: 0 })
const measure = useCallback(() => {
const el = ref.current
if (!el) return
const rect = el.getBoundingClientRect()
setSize({ width: rect.width, height: rect.height })
}, [ref])
useEffect(() => {
const el = ref.current
if (!el) return
measure()
const RO =
el.ownerDocument.defaultView?.ResizeObserver ?? globalThis.ResizeObserver
if (!RO) return
const observer = new RO((entries) => {
const entry = entries[0]
if (!entry) return
const { width, height } = entry.contentRect
setSize({ width, height })
})
observer.observe(el)
return () => observer.disconnect()
}, [ref, measure])
return size
}
@@ -1,91 +0,0 @@
import { useLayoutEffect, useRef, type RefObject } from 'react'
type Options = {
/** Animation duration in ms. */
duration?: number
/** CSS easing function. */
easing?: string
}
/**
* FLIP (First, Last, Invert, Play) animation hook.
*
* For every keyed direct child of `containerRef`, records its position
* before a render (the "first" rect) and, once the DOM has committed, plays
* an inverse transform back to the identity position. The effect is a
* smooth slide whenever tiles are added, removed, reordered, or a new grid
* shape shifts them.
*
* Safe to call inside a Document PiP window: uses the element's own
* Web Animations API (element.animate) which lives in the PiP document.
* Respects `prefers-reduced-motion` and no-ops on the first mount.
*/
export const usePipFlipAnimations = <T extends HTMLElement>(
containerRef: RefObject<T | null>,
keys: ReadonlyArray<string>,
{ duration = 220, easing = 'cubic-bezier(0.2, 0, 0, 1)' }: Options = {}
) => {
const prevRectsRef = useRef<Map<string, DOMRect>>(new Map())
const firstRunRef = useRef(true)
useLayoutEffect(() => {
const container = containerRef.current
if (!container) return
const doc = container.ownerDocument
const view = doc.defaultView
const reduceMotion = view?.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches
const children = Array.from(container.children) as HTMLElement[]
const nextRects = new Map<string, DOMRect>()
children.forEach((el, i) => {
const key = keys[i]
if (!key) return
nextRects.set(key, el.getBoundingClientRect())
})
if (firstRunRef.current) {
firstRunRef.current = false
prevRectsRef.current = nextRects
return
}
if (!reduceMotion) {
children.forEach((el, i) => {
const key = keys[i]
if (!key) return
const prev = prevRectsRef.current.get(key)
const next = nextRects.get(key)
if (!prev || !next) return
const dx = prev.left - next.left
const dy = prev.top - next.top
const sx = next.width === 0 ? 1 : prev.width / next.width
const sy = next.height === 0 ? 1 : prev.height / next.height
// Skip no-ops: sub-pixel shifts don't benefit from animation.
if (
Math.abs(dx) < 1 &&
Math.abs(dy) < 1 &&
Math.abs(sx - 1) < 0.01 &&
Math.abs(sy - 1) < 0.01
)
return
el.animate(
[
{
transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`,
},
{ transform: 'translate(0, 0) scale(1, 1)' },
],
{ duration, easing, fill: 'backwards' }
)
})
}
prevRectsRef.current = nextRects
}, [containerRef, duration, easing, keys])
}
@@ -1,45 +0,0 @@
import { useEffect, type RefObject } from 'react'
import { keyboardShortcutsStore } from '@/stores/keyboardShortcuts'
import { formatShortcutKey } from '@/features/shortcuts/utils'
import { isMacintosh } from '@/utils/livekit'
/**
* Mirror the main-window keyboard shortcuts inside the PiP document.
*
* The central `useKeyboardShortcuts` hook listens on `window`, which is the
* main document's window. Keydown events from the PiP document never reach
* it. This hook attaches the same dispatch logic to the PiP document so that
* Ctrl+D (mic), Ctrl+E (cam), etc. work identically in both contexts.
*/
export const usePipKeyboardShortcuts = (
containerRef: RefObject<HTMLElement | null>
) => {
useEffect(() => {
const doc = containerRef.current?.ownerDocument
if (!doc || doc === document) return
const onKeyDown = (e: KeyboardEvent) => {
const { key, metaKey, ctrlKey, shiftKey, altKey } = e
if (!key) return
const shortcutKey = formatShortcutKey({
key,
ctrlKey: ctrlKey || (isMacintosh() && metaKey),
shiftKey,
altKey,
})
let handler = keyboardShortcutsStore.shortcuts.get(shortcutKey)
if (!handler && shortcutKey === 'ctrl+shift+?') {
handler = keyboardShortcutsStore.shortcuts.get('ctrl+shift+/')
}
if (!handler) return
e.preventDefault()
handler()
}
doc.addEventListener('keydown', onKeyDown)
return () => doc.removeEventListener('keydown', onKeyDown)
}, [containerRef])
}
@@ -1,41 +0,0 @@
import { useEffect, useRef, type RefObject } from 'react'
type Options = {
/** Remap the captured trigger (e.g. when it unmounts on click). */
resolveTrigger?: (activeEl: HTMLElement | null) => HTMLElement | null
}
/**
* `useRestoreFocus`: captures and restores focus via the PiP
* document instead of the main one.
*/
export const usePipRestoreFocus = (
ref: RefObject<HTMLElement | null>,
isOpen: boolean,
{ resolveTrigger }: Options = {}
) => {
const prevOpenRef = useRef(false)
const triggerRef = useRef<HTMLElement | null>(null)
useEffect(() => {
const doc = ref.current?.ownerDocument
const wasOpen = prevOpenRef.current
prevOpenRef.current = isOpen
if (!doc) return
if (!wasOpen && isOpen) {
const activeEl = doc.activeElement as HTMLElement | null
triggerRef.current = resolveTrigger ? resolveTrigger(activeEl) : activeEl
return
}
if (wasOpen && !isOpen) {
const trigger = triggerRef.current
triggerRef.current = null
if (trigger && doc.contains(trigger)) {
requestAnimationFrame(() => trigger.focus({ preventScroll: true }))
}
}
}, [ref, isOpen, resolveTrigger])
}
@@ -1,29 +0,0 @@
import { useCallback } from 'react'
import { useSnapshot } from 'valtio'
import { roomPiPStore } from '@/stores/roomPiP'
export const useRoomPiP = () => {
const { isOpen } = useSnapshot(roomPiPStore)
const isSupported =
typeof globalThis !== 'undefined' && 'documentPictureInPicture' in globalThis
const open = useCallback(() => {
roomPiPStore.isOpen = true
}, [])
const close = useCallback(() => {
roomPiPStore.isOpen = false
}, [])
const toggle = useCallback(() => {
roomPiPStore.isOpen = !roomPiPStore.isOpen
}, [])
return {
isSupported,
isOpen,
open,
close,
toggle,
}
}
@@ -1,19 +0,0 @@
import { proxy } from 'valtio'
import type { PanelId, SubPanelId } from '@/features/rooms/livekit/types/panel'
type PipLayoutState = {
activePanelId: PanelId | null
activeSubPanelId: SubPanelId | null
showReactionsToolbar: boolean
}
/**
* Separate layout store for the PiP window.
* Decouples PiP side panel state from the main view so opening Chat/Info/etc.
* in PiP does not affect the main window and vice versa.
*/
export const pipLayoutStore = proxy<PipLayoutState>({
activePanelId: null,
activeSubPanelId: null,
showReactionsToolbar: false,
})
@@ -1,114 +0,0 @@
export type PipTilePlacement = {
gridColumn: string
gridRow: number
}
export type PipGridLayout = {
cols: number
rows: number
/** Number of CSS sub-columns; use as `repeat(subColumns, 1fr)`. */
subColumns: number
/** One entry per tile, in input order. */
placements: PipTilePlacement[]
}
/**
* Target tile aspect ratio used to score candidate grid shapes.
*
* Video sources are 16:9, but picking 16:9 as the target makes the
* scorer indifferent between a stretched 2-col slab (aspect ~2.7) and a
* squarer 3-col tile (aspect ~1.2) because log distance is symmetric.
* The UI works better with square, face-friendly tiles. This target keeps
* wide windows from collapsing to 2 columns with short, stretched rows
* and pushes the scorer to add a column instead.
*/
const TARGET_TILE_ASPECT = 1
/**
* Smallest count from which we force at least two columns.
* For 1-3 participants it is acceptable to stack vertically in tall
* windows, but from 4 people onwards we keep >=2 columns to
* avoid endless vertical scrolling; the scorer handles the rest.
*/
const FORCE_TWO_COLS_COUNT = 4
const pickGridShape = (
count: number,
width: number,
height: number
): { cols: number; rows: number } => {
if (count <= 1) return { cols: 1, rows: Math.max(1, count) }
if (width <= 0 || height <= 0) return { cols: count, rows: 1 }
const minCols = count >= FORCE_TWO_COLS_COUNT ? 2 : 1
let best = {
cols: minCols,
rows: Math.ceil(count / minCols),
score: -Infinity,
}
for (let cols = minCols; cols <= count; cols++) {
const rows = Math.ceil(count / cols)
const tileW = width / cols
const tileH = height / rows
if (tileW <= 0 || tileH <= 0) continue
// Score: aspect close to target, few empty cells, large tile area,
// and a tiny bias toward fewer rows so ties (perfectly square shapes)
// resolve in favour of a shorter, wider grid.
const aspectScore = -Math.abs(Math.log(tileW / tileH / TARGET_TILE_ASPECT))
const emptyCells = cols * rows - count
const fillScore = -emptyCells * 0.1
const areaScore = Math.log(tileW * tileH) * 0.5
const rowsPenalty = -rows * 0.01
const score = aspectScore * 2 + fillScore + areaScore + rowsPenalty
if (score > best.score) best = { cols, rows, score }
}
return { cols: best.cols, rows: best.rows }
}
/**
* Pure function. Given a tile count and stage dimensions, returns the CSS
* grid layout for the PiP stage:
*
* - picks a cols x rows shape close to 16:9 tiles,
* - stretches any partial last row so its tiles share the full row width
* (no empty cells, no small centered tile).
*
* Callers consume the result directly: `subColumns` feeds
* `grid-template-columns: repeat(N, 1fr)` and each tile reads its own
* `gridColumn`/`gridRow` from `placements`.
*/
export const computePipGridLayout = (
count: number,
width: number,
height: number
): PipGridLayout => {
if (count <= 0) {
return { cols: 1, rows: 1, subColumns: 1, placements: [] }
}
const { cols, rows } = pickGridShape(count, width, height)
const tilesInLastRow = count - cols * (rows - 1)
const hasPartialRow = tilesInLastRow > 0 && tilesInLastRow < cols
const subColumns = hasPartialRow ? cols * tilesInLastRow : cols
const fullRowSpan = hasPartialRow ? tilesInLastRow : 1
const lastRowSpan = hasPartialRow ? cols : 1
const placements: PipTilePlacement[] = []
for (let i = 0; i < count; i++) {
const row = Math.floor(i / cols)
const colIndex = i % cols
const isLastRow = row === rows - 1 && hasPartialRow
const span = isLastRow ? lastRowSpan : fullRowSpan
const colStart = colIndex * span + 1
placements.push({
gridColumn: `${colStart} / span ${span}`,
gridRow: row + 1,
})
}
return { cols, rows, subColumns, placements }
}
@@ -1,62 +0,0 @@
import { Emoji } from '@/features/reactions/types'
export const EMOJI_SLOT_WIDTH = 40
export const ARROW_SLOT_WIDTH = 32
export const PILL_HORIZONTAL_PADDING = 12
export const WRAPPER_HORIZONTAL_PADDING = 16
const EMOJIS = Object.values(Emoji)
export type ReactionsPage = {
visibleEmojis: Emoji[]
hasOverflow: boolean
canGoLeft: boolean
canGoRight: boolean
visibleCount: number
}
/**
* Compute how many emojis fit in `availableWidth` and slice the visible page.
* Arrow slots are reserved only when the list overflows.
*/
export const computeReactionsPage = (
availableWidth: number,
pageStart: number
): ReactionsPage => {
const usableWidth =
availableWidth - WRAPPER_HORIZONTAL_PADDING - PILL_HORIZONTAL_PADDING
const maxWithoutArrows = Math.max(
1,
Math.floor(usableWidth / EMOJI_SLOT_WIDTH)
)
if (EMOJIS.length <= maxWithoutArrows) {
return {
visibleEmojis: EMOJIS,
hasOverflow: false,
canGoLeft: false,
canGoRight: false,
visibleCount: EMOJIS.length,
}
}
const visibleCount = Math.max(
1,
Math.floor((usableWidth - ARROW_SLOT_WIDTH * 2) / EMOJI_SLOT_WIDTH)
)
const clampedStart = Math.min(
Math.max(0, pageStart),
Math.max(0, EMOJIS.length - visibleCount)
)
return {
visibleEmojis: EMOJIS.slice(clampedStart, clampedStart + visibleCount),
hasOverflow: true,
canGoLeft: clampedStart > 0,
canGoRight: clampedStart + visibleCount < EMOJIS.length,
visibleCount,
}
}
export const getMaxPageStart = (visibleCount: number): number =>
Math.max(0, EMOJIS.length - visibleCount)
@@ -1,48 +0,0 @@
import {
isTrackReference,
TrackReferenceOrPlaceholder,
} from '@livekit/components-core'
import { Track } from 'livekit-client'
/**
* Helpers used by the PiP layouts to classify/pick tracks.
* Kept free of React so they are trivially testable and cheap to call.
*/
export const pickScreenShareTrack = (
tracks: TrackReferenceOrPlaceholder[]
): TrackReferenceOrPlaceholder | undefined =>
tracks
.filter((track) => isTrackReference(track))
.find((track) => track.publication.source === Track.Source.ScreenShare)
export const pickLocalCameraTrack = (
tracks: TrackReferenceOrPlaceholder[]
): TrackReferenceOrPlaceholder | undefined =>
tracks.find(
(track) =>
track.source === Track.Source.Camera && track.participant?.isLocal
)
export const pickRemoteCameraTrack = (
tracks: TrackReferenceOrPlaceholder[]
): TrackReferenceOrPlaceholder | undefined =>
tracks.find(
(track) =>
track.source === Track.Source.Camera && !track.participant?.isLocal
)
export const isCameraTrack = (track: TrackReferenceOrPlaceholder): boolean =>
track.source === Track.Source.Camera
/**
* Produces a stable React key for a track so resizes/reshuffles of the grid
* do not remount the underlying <video> element.
*/
export const getTrackKey = (track: TrackReferenceOrPlaceholder): string => {
const identity = track.participant?.identity ?? 'unknown'
if (isTrackReference(track)) {
return `${identity}::${track.source}::${track.publication.trackSid}`
}
return `${identity}::${track.source}::placeholder`
}
@@ -1,33 +0,0 @@
import { useTranslation } from 'react-i18next'
import { RiEmotionLine } from '@remixicon/react'
import { ToggleButton } from '@/primitives'
import { useRegisterKeyboardShortcut } from '@/features/shortcuts/useRegisterKeyboardShortcut'
import { useReactionsToolbar } from '../hooks/useReactionsToolbar'
export const ReactionsToggle = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { isOpen, toggle } = useReactionsToolbar()
useRegisterKeyboardShortcut({
id: 'reaction',
handler: toggle,
})
return (
<ToggleButton
id="reactions-toggle"
data-attr="reactions-toggle"
square
variant="primaryDark"
aria-label={t('button')}
aria-expanded={isOpen}
tooltip={t('button')}
isSelected={isOpen}
onChange={toggle}
>
<RiEmotionLine />
</ToggleButton>
)
}
@@ -1,31 +0,0 @@
import { useTranslation } from 'react-i18next'
import { css } from '@/styled-system/css'
import { Emoji } from '../../types'
import { useReactions } from '../../hooks/useReactions'
import { Button } from '@/primitives'
export const ReactionButton = ({ emoji }: { emoji: Emoji }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { sendReaction } = useReactions()
return (
<Button
onPress={() => sendReaction(emoji)}
aria-label={t(`emojis.${emoji}`)}
variant="primaryTextDark"
size="sm"
round
data-attr={`send-reaction-${emoji}`}
>
<img
src={`/assets/reactions/${emoji}.png`}
alt=""
className={css({
width: '28px',
height: '28px',
pointerEvents: 'none',
userSelect: 'none',
})}
/>
</Button>
)
}
@@ -1,153 +0,0 @@
import { FocusScope, useFocusManager } from '@react-aria/focus'
import { useReactionsToolbar } from '../../hooks/useReactionsToolbar'
import { ReactionButton } from './ReactionButton'
import { Emoji } from '../../types'
import { styled } from '@/styled-system/jsx'
import { layoutStore } from '@/stores/layout'
import { getFirstControlBarFocusable } from '@/utils/dom'
import { useIsMobile } from '@/utils/useIsMobile'
import { useEffect, useRef, useState } from 'react'
import { useDelayUnmount } from '@/hooks/useDelayUnmount'
import { useTranslation } from 'react-i18next'
const Container = styled('div', {
base: {
display: 'flex',
justifyContent: 'center',
position: 'absolute',
bottom: 'var(--sizes-room-control-bar)',
left: 0,
right: 0,
pointerEvents: 'none',
},
})
const StyledStrip = styled('div', {
base: {
display: 'flex',
gap: '0.2rem',
borderRadius: '21px',
padding: '0.15rem',
backgroundColor: 'primaryDark.100',
opacity: 0,
transform: 'translateY(3.25rem)',
transition: 'opacity, transform',
transitionDuration: '0.5s',
transitionTimingFunction: 'cubic-bezier(0.4, 0, 0.2, 1)',
pointerEvents: 'none',
},
variants: {
isVisible: {
true: {
opacity: 1,
transform: 'translateY(0)',
pointerEvents: 'auto',
},
},
desktopOffset: {
true: {
// Ideally this value should be calculated dynamically in JavaScript to keep
// the reaction toolbar perfectly centered relative to the reaction toggle.
// However, for simplicity and to follow a pragmatic 80/20 approach,
// this value is currently hardcoded in CSS.
marginRight: '30px',
},
},
},
})
const Strip = ({ children }: { children: React.ReactNode }) => {
const { isOpen } = useReactionsToolbar()
const isMobile = useIsMobile()
const ref = useRef<HTMLDivElement>(null)
const [isVisible, setIsVisible] = useState(false)
useEffect(() => {
if (isOpen) {
// defer one frame so the browser paints opacity:0 first
const id = requestAnimationFrame(() => setIsVisible(true))
return () => cancelAnimationFrame(id)
} else {
setIsVisible(false)
}
}, [isOpen])
return (
<StyledStrip
ref={ref}
aria-hidden={!isOpen}
isVisible={isVisible}
desktopOffset={!isMobile}
>
{children}
</StyledStrip>
)
}
const KeyboardNavigation = ({ children }: { children: React.ReactNode }) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const focusManager = useFocusManager()
const onFocus = (e: React.FocusEvent<HTMLDivElement>) => {
const comingFromOutside = !e.currentTarget.contains(e.relatedTarget)
if (comingFromOutside) {
focusManager?.focusFirst()
}
}
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
switch (e.key) {
case 'ArrowRight':
focusManager?.focusNext({ wrap: true })
break
case 'ArrowLeft':
focusManager?.focusPrevious({ wrap: true })
break
case 'Escape':
e.preventDefault()
document.getElementById('reactions-toggle')?.focus()
layoutStore.showReactionsToolbar = false
break
case 'Tab':
if (!e.shiftKey) {
e.preventDefault()
getFirstControlBarFocusable('control-bar')?.focus()
}
break
}
}
return (
<div
role="toolbar"
aria-label={t('toolbar')}
onKeyDown={onKeyDown}
onFocus={onFocus}
>
{children}
</div>
)
}
export const ReactionsToolbar = () => {
const { isOpen } = useReactionsToolbar()
const shouldMount = useDelayUnmount(isOpen, 300)
if (!shouldMount) return null
return (
<Container>
{/* eslint-disable-next-line jsx-a11y/no-autofocus*/}
<FocusScope autoFocus>
<KeyboardNavigation>
<Strip>
{Object.values(Emoji).map((emoji) => (
<ReactionButton key={emoji} emoji={emoji} />
))}
</Strip>
</KeyboardNavigation>
</FocusScope>
</Container>
)
}
@@ -1,5 +0,0 @@
export const ANIMATION_DURATION = 3000
export const ANIMATION_DISTANCE = 300
export const FADE_OUT_THRESHOLD = 0.7
export const REACTION_SPAWN_WIDTH_RATIO = 0.2
export const INITIAL_POSITION = 200
@@ -1,26 +0,0 @@
import { useState, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
import { accessibilityStore } from '@/stores/accessibility'
import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { getEmojiLabel } from '../utils'
import { Reaction } from '../types'
export const useAnnounceReaction = (latestReaction: Reaction | undefined) => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { announceReactions } = useSnapshot(accessibilityStore)
const [lastAnnouncedId, setLastAnnouncedId] = useState<string | null>(null)
const announce = useScreenReaderAnnounce()
useEffect(() => {
if (!announceReactions || !latestReaction) return
if (latestReaction.id === lastAnnouncedId) return
const emojiLabel = getEmojiLabel(latestReaction.emoji, t)
const participantName = latestReaction.participantName
announce(t('announce', { name: participantName, emoji: emojiLabel }))
setLastAnnouncedId(latestReaction.id)
}, [announce, latestReaction, lastAnnouncedId, announceReactions, t])
}
@@ -1,56 +0,0 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { reactionsStore } from '@/stores/reactions'
import { NotificationType } from '@/features/notifications/NotificationType'
import { useNotifyParticipants } from '@/features/notifications'
import useRateLimiter from '@/hooks/useRateLimiter'
import { Participant } from 'livekit-client'
import { Emoji } from '../types'
import { ANIMATION_DURATION } from '../constants'
export const useReactions = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const { notifyParticipants } = useNotifyParticipants()
const appendReaction = useCallback(
(emoji: Emoji, participant?: Participant) => {
const newReaction = {
id: `${emoji}-${Date.now()}-${Math.random()}`,
emoji,
participantName: participant
? participant.name || participant.identity
: t('you'),
isLocal: !participant,
}
reactionsStore.reactions.push(newReaction)
setTimeout(() => {
const index = reactionsStore.reactions.findIndex(
(r) => r.id === newReaction.id
)
if (index !== -1) reactionsStore.reactions.splice(index, 1)
}, ANIMATION_DURATION)
},
[t]
)
const sendReaction = async (emoji: Emoji) => {
appendReaction(emoji)
await notifyParticipants({
type: NotificationType.ReactionReceived,
additionalData: { data: { emoji } },
})
}
const debouncedSendReaction = useRateLimiter({
callback: sendReaction,
maxCalls: 10,
windowMs: 1000,
})
return {
sendReaction: debouncedSendReaction,
appendReaction,
}
}
@@ -1,13 +0,0 @@
import { useSnapshot } from 'valtio'
import { layoutStore } from '@/stores/layout'
export const useReactionsToolbar = () => {
const layoutSnap = useSnapshot(layoutStore)
return {
isOpen: layoutSnap.showReactionsToolbar,
toggle: () => {
layoutStore.showReactionsToolbar = !layoutSnap.showReactionsToolbar
},
}
}
@@ -1,17 +0,0 @@
export enum Emoji {
THUMBS_UP = 'thumbs-up',
THUMBS_DOWN = 'thumbs-down',
CLAP = 'clapping-hands',
HEART = 'red-heart',
LAUGHING = 'face-with-tears-of-joy',
SURPRISED = 'face-with-open-mouth',
CELEBRATION = 'party-popper',
PLEASE = 'folded-hands',
}
export interface Reaction {
id: string
emoji: Emoji
participantName: string
isLocal: boolean
}
@@ -215,8 +215,8 @@ export const Conference = ({
audio={userConfig.audioEnabled} audio={userConfig.audioEnabled}
video={ video={
userConfig.videoEnabled && { userConfig.videoEnabled && {
processor: BackgroundProcessorFactory.fromProcessorConfig( processor: BackgroundProcessorFactory.deserializeProcessor(
userConfig.processorConfig userConfig.processorSerialized
), ),
} }
} }
@@ -4,15 +4,15 @@ import { css } from '@/styled-system/css'
import { Screen } from '@/layout/Screen' import { Screen } from '@/layout/Screen'
import { useEffect, useMemo, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { import {
createLocalAudioTrack,
createLocalVideoTrack, createLocalVideoTrack,
createLocalAudioTrack,
LocalAudioTrack, LocalAudioTrack,
LocalVideoTrack, LocalVideoTrack,
Track, Track,
} from 'livekit-client' } from 'livekit-client'
import { H } from '@/primitives/H' import { H } from '@/primitives/H'
import { Field } from '@/primitives/Field' import { Field } from '@/primitives/Field'
import { Button, Dialog, Form, Text } from '@/primitives' import { Button, Dialog, Text, Form } from '@/primitives'
import { VStack } from '@/styled-system/jsx' import { VStack } from '@/styled-system/jsx'
import { Heading } from 'react-aria-components' import { Heading } from 'react-aria-components'
import { RiImageCircleAiFill } from '@remixicon/react' import { RiImageCircleAiFill } from '@remixicon/react'
@@ -44,7 +44,8 @@ const onError = (e: Error) => console.error('ERROR', e)
const Effects = ({ const Effects = ({
videoTrack, videoTrack,
}: Pick<EffectsConfigurationProps, 'videoTrack'>) => { onSubmit,
}: Pick<EffectsConfigurationProps, 'videoTrack' | 'onSubmit'>) => {
const { t } = useTranslation('rooms', { keyPrefix: 'join.effects' }) const { t } = useTranslation('rooms', { keyPrefix: 'join.effects' })
const [isDialogOpen, setIsDialogOpen] = useState(false) const [isDialogOpen, setIsDialogOpen] = useState(false)
const openDialog = () => setIsDialogOpen(true) const openDialog = () => setIsDialogOpen(true)
@@ -80,7 +81,7 @@ const Effects = ({
> >
{t('subTitle')} {t('subTitle')}
</Text> </Text>
<EffectsConfiguration videoTrack={videoTrack} /> <EffectsConfiguration videoTrack={videoTrack} onSubmit={onSubmit} />
</Dialog> </Dialog>
<Button <Button
variant="whiteCircle" variant="whiteCircle"
@@ -110,7 +111,7 @@ export const Join = ({
audioDeviceId, audioDeviceId,
audioOutputDeviceId, audioOutputDeviceId,
videoDeviceId, videoDeviceId,
processorConfig, processorSerialized,
username, username,
}, },
saveAudioInputEnabled, saveAudioInputEnabled,
@@ -119,6 +120,7 @@ export const Join = ({
saveAudioInputDeviceId, saveAudioInputDeviceId,
saveVideoInputDeviceId, saveVideoInputDeviceId,
saveUsername, saveUsername,
saveProcessorSerialized,
} = usePersistentUserChoices() } = usePersistentUserChoices()
const initialUserChoices = useRef<LocalUserChoices | null>(null) const initialUserChoices = useRef<LocalUserChoices | null>(null)
@@ -130,7 +132,7 @@ export const Join = ({
audioDeviceId, audioDeviceId,
audioOutputDeviceId, audioOutputDeviceId,
videoDeviceId, videoDeviceId,
processorConfig, processorSerialized,
username, username,
} }
} }
@@ -144,8 +146,8 @@ export const Join = ({
video: !!initialUserChoices.current && video: !!initialUserChoices.current &&
initialUserChoices.current?.videoEnabled && { initialUserChoices.current?.videoEnabled && {
deviceId: initialUserChoices.current.videoDeviceId, deviceId: initialUserChoices.current.videoDeviceId,
processor: BackgroundProcessorFactory.fromProcessorConfig( processor: BackgroundProcessorFactory.deserializeProcessor(
initialUserChoices.current.processorConfig initialUserChoices.current.processorSerialized
), ),
}, },
}, },
@@ -184,7 +186,9 @@ export const Join = ({
const track = await createLocalVideoTrack({ const track = await createLocalVideoTrack({
deviceId: { exact: videoDeviceId }, deviceId: { exact: videoDeviceId },
processor: processor:
BackgroundProcessorFactory.fromProcessorConfig(processorConfig), BackgroundProcessorFactory.deserializeProcessor(
processorSerialized
),
}) })
setDynamicVideoTrack(track) setDynamicVideoTrack(track)
} catch (error) { } catch (error) {
@@ -203,7 +207,7 @@ export const Join = ({
}, [ }, [
videoEnabled, videoEnabled,
videoDeviceId, videoDeviceId,
processorConfig, processorSerialized,
previewVideoTrack, previewVideoTrack,
dynamicVideoTrack, dynamicVideoTrack,
]) ])
@@ -686,7 +690,12 @@ export const Join = ({
zIndex: '1', zIndex: '1',
})} })}
> >
<Effects videoTrack={videoTrack} /> <Effects
videoTrack={videoTrack}
onSubmit={(processor) =>
saveProcessorSerialized(processor?.serialize())
}
/>
</div> </div>
</div> </div>
</div> </div>
@@ -32,7 +32,6 @@ export const AdminToggle = ({
aria-label={t(tooltipLabel)} aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)} tooltip={t(tooltipLabel)}
isSelected={isAdminOpen} isSelected={isAdminOpen}
aria-expanded={isAdminOpen}
onPress={(e) => { onPress={(e) => {
toggleAdmin() toggleAdmin()
onPress?.(e) onPress?.(e)
@@ -31,8 +31,6 @@ import { FullScreenShareWarning } from './FullScreenShareWarning'
import { ParticipantName } from './ParticipantName' import { ParticipantName } from './ParticipantName'
import { getParticipantName } from '@/features/rooms/utils/getParticipantName' import { getParticipantName } from '@/features/rooms/utils/getParticipantName'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { getShortcutDescriptorById } from '@/features/shortcuts/catalog'
import { formatShortcutLabel } from '@/features/shortcuts/formatLabels'
import { KeyboardShortcutHint } from './KeyboardShortcutHint' import { KeyboardShortcutHint } from './KeyboardShortcutHint'
export function TrackRefContextIfNeeded( export function TrackRefContextIfNeeded(
@@ -239,13 +237,7 @@ export const ParticipantTile: (
)} )}
</ParticipantContextIfNeeded> </ParticipantContextIfNeeded>
</TrackRefContextIfNeeded> </TrackRefContextIfNeeded>
<KeyboardShortcutHint> <KeyboardShortcutHint>{t('toolbarHint')}</KeyboardShortcutHint>
{t('toolbarHint', {
shortcut: formatShortcutLabel(
getShortcutDescriptorById('open-shortcuts')?.shortcut
),
})}
</KeyboardShortcutHint>
</div> </div>
) )
}) })
@@ -2,17 +2,19 @@ import { createPortal } from 'react-dom'
import { useState, useEffect, useMemo } from 'react' import { useState, useEffect, useMemo } from 'react'
import { Text } from '@/primitives' import { Text } from '@/primitives'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { Participant } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { Reaction } from '@/features/rooms/livekit/components/controls/ReactionsToggle'
import { getEmojiLabel } from '@/features/rooms/livekit/utils/reactionUtils'
import { accessibilityStore } from '@/stores/accessibility'
import { useSnapshot } from 'valtio' import { useSnapshot } from 'valtio'
import { reactionsStore } from '@/stores/reactions' import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
import { useAnnounceReaction } from '../hooks/useAnnounceReaction'
import { Reaction } from '../types' export const ANIMATION_DURATION = 3000
import { export const ANIMATION_DISTANCE = 300
ANIMATION_DISTANCE, export const FADE_OUT_THRESHOLD = 0.7
ANIMATION_DURATION, export const REACTION_SPAWN_WIDTH_RATIO = 0.2
FADE_OUT_THRESHOLD, export const INITIAL_POSITION = 200
INITIAL_POSITION,
REACTION_SPAWN_WIDTH_RATIO,
} from '../constants'
interface FloatingReactionProps { interface FloatingReactionProps {
emoji: string emoji: string
@@ -77,7 +79,7 @@ export function FloatingReaction({
> >
<img <img
src={`/assets/reactions/${emoji}.png`} src={`/assets/reactions/${emoji}.png`}
alt="" alt={''}
className={css({ className={css({
height: '50px', height: '50px',
})} })}
@@ -114,7 +116,14 @@ export function FloatingReaction({
) )
} }
const ReactionPortal = ({ reaction }: { reaction: Reaction }) => { export function ReactionPortal({
emoji,
participant,
}: {
emoji: string
participant: Participant
}) {
const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const speed = useMemo(() => Math.random() * 1.5 + 0.5, []) const speed = useMemo(() => Math.random() * 1.5 + 0.5, [])
const scale = useMemo(() => Math.max(Math.random() + 0.5, 1), []) const scale = useMemo(() => Math.max(Math.random() + 0.5, 1), [])
return createPortal( return createPortal(
@@ -129,27 +138,51 @@ const ReactionPortal = ({ reaction }: { reaction: Reaction }) => {
})} })}
> >
<FloatingReaction <FloatingReaction
emoji={reaction.emoji} emoji={emoji}
speed={speed} speed={speed}
scale={scale} scale={scale}
name={reaction.participantName} name={participant?.isLocal ? t('you') : participant.name}
isLocal={reaction.isLocal} isLocal={participant?.isLocal}
/> />
</div>, </div>,
document.body document.body
) )
} }
export const ReactionPortals = () => { export const ReactionPortals = ({ reactions }: { reactions: Reaction[] }) => {
const { reactions } = useSnapshot(reactionsStore) const { t } = useTranslation('rooms', { keyPrefix: 'controls.reactions' })
const latestReaction = reactions.at(-1) const { announceReactions } = useSnapshot(accessibilityStore)
const [lastAnnouncedId, setLastAnnouncedId] = useState<number | null>(null)
const announce = useScreenReaderAnnounce()
useAnnounceReaction(latestReaction) const latestReaction =
reactions.length > 0 ? reactions[reactions.length - 1] : undefined
useEffect(() => {
if (!announceReactions) {
return
}
if (!latestReaction) return
const isNewReaction = latestReaction.id !== lastAnnouncedId
if (!isNewReaction) return
const emojiLabel = getEmojiLabel(latestReaction.emoji, t)
const participantName = latestReaction.participant?.isLocal
? t('you')
: latestReaction.participant?.name?.trim() ||
t('someone', { defaultValue: 'Someone' })
announce(t('announce', { name: participantName, emoji: emojiLabel }))
setLastAnnouncedId(latestReaction.id)
}, [announce, latestReaction, lastAnnouncedId, announceReactions, t])
return ( return (
<> <>
{reactions.map((instance) => ( {reactions.map((instance) => (
<ReactionPortal key={instance.id} reaction={instance} /> <ReactionPortal
key={instance.id}
emoji={instance.emoji}
participant={instance.participant}
/>
))} ))}
</> </>
) )
@@ -1,3 +1,4 @@
import { layoutStore } from '@/stores/layout'
import { css } from '@/styled-system/css' import { css } from '@/styled-system/css'
import { Heading } from 'react-aria-components' import { Heading } from 'react-aria-components'
import { text } from '@/primitives/Text' import { text } from '@/primitives/Text'
@@ -5,7 +6,7 @@ import { Button, Div } from '@/primitives'
import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react' import { RiArrowLeftLine, RiCloseLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { ParticipantsList } from './controls/Participants/ParticipantsList' import { ParticipantsList } from './controls/Participants/ParticipantsList'
import { type SidePanelStore, useSidePanel } from '../hooks/useSidePanel' import { useSidePanel } from '../hooks/useSidePanel'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { Chat } from '../prefabs/Chat' import { Chat } from '../prefabs/Chat'
import { Effects } from './effects/Effects' import { Effects } from './effects/Effects'
@@ -13,7 +14,6 @@ import { Admin } from './Admin'
import { Tools } from './Tools' import { Tools } from './Tools'
import { Info } from './Info' import { Info } from './Info'
import { HStack } from '@/styled-system/jsx' import { HStack } from '@/styled-system/jsx'
import { useReactionsToolbar } from '@/features/reactions/hooks/useReactionsToolbar'
type StyledSidePanelProps = { type StyledSidePanelProps = {
title: string title: string
@@ -25,7 +25,6 @@ type StyledSidePanelProps = {
isSubmenu: boolean isSubmenu: boolean
onBack: () => void onBack: () => void
backButtonLabel: string backButtonLabel: string
isReactionToolbarOpen?: boolean
} }
const StyledSidePanel = ({ const StyledSidePanel = ({
@@ -34,7 +33,6 @@ const StyledSidePanel = ({
children, children,
onClose, onClose,
isClosed, isClosed,
isReactionToolbarOpen,
closeButtonTooltip, closeButtonTooltip,
isSubmenu = false, isSubmenu = false,
onBack, onBack,
@@ -53,23 +51,17 @@ const StyledSidePanel = ({
overflow: 'hidden', overflow: 'hidden',
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
margin: 'var(--sizes-room-side-panel-margin)', margin: '1.5rem 1.5rem 1.5rem 0',
marginLeft: 0,
marginBottom: 0,
padding: 0, padding: 0,
gap: 0, gap: 0,
right: 0, right: 0,
top: 0, top: 0,
width: 'var(--sizes-room-side-panel)', bottom: '80px',
width: '360px',
transition: '.5s cubic-bezier(.4,0,.2,1) 5ms', transition: '.5s cubic-bezier(.4,0,.2,1) 5ms',
})} })}
style={{ style={{
transform: isClosed transform: isClosed ? 'translateX(calc(360px + 1.5rem))' : 'none',
? 'translateX(calc(var(--sizes-room-side-panel) + var(--sizes-room-side-panel-margin)))'
: 'none',
bottom: isReactionToolbarOpen
? 'calc( var(--sizes-room-control-bar) + var(--sizes-room-reaction-toolbar-height) + calc(var(--lk-grid-gap) / 2))'
: 'var(--sizes-room-control-bar)',
}} }}
aria-hidden={isClosed} aria-hidden={isClosed}
aria-label={ariaLabel} aria-label={ariaLabel}
@@ -143,7 +135,7 @@ const Panel = ({ isOpen, keepAlive = false, children }: PanelProps) => (
{keepAlive || isOpen ? children : null} {keepAlive || isOpen ? children : null}
</div> </div>
) )
export const SidePanel = ({ store }: { store?: SidePanelStore }) => { export const SidePanel = () => {
const { const {
activePanelId, activePanelId,
isParticipantsOpen, isParticipantsOpen,
@@ -155,27 +147,24 @@ export const SidePanel = ({ store }: { store?: SidePanelStore }) => {
isInfoOpen, isInfoOpen,
isSubPanelOpen, isSubPanelOpen,
activeSubPanelId, activeSubPanelId,
closePanel, } = useSidePanel()
goBack,
} = useSidePanel(store)
const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' }) const { t } = useTranslation('rooms', { keyPrefix: 'sidePanel' })
const title = t(`heading.${activeSubPanelId || activePanelId}`)
const { isOpen: isReactionToolbarOpen } = useReactionsToolbar()
return ( return (
<StyledSidePanel <StyledSidePanel
title={title} title={t(`heading.${activeSubPanelId || activePanelId}`)}
ariaLabel={t('ariaLabel', { title })} ariaLabel={t('ariaLabel')}
onClose={closePanel} onClose={() => {
layoutStore.activePanelId = null
layoutStore.activeSubPanelId = null
}}
closeButtonTooltip={t('closeButton', { closeButtonTooltip={t('closeButton', {
content: t(`content.${activeSubPanelId || activePanelId}`), content: t(`content.${activeSubPanelId || activePanelId}`),
})} })}
isClosed={!isSidePanelOpen} isClosed={!isSidePanelOpen}
isSubmenu={isSubPanelOpen} isSubmenu={isSubPanelOpen}
isReactionToolbarOpen={isReactionToolbarOpen}
backButtonLabel={t('backToTools')} backButtonLabel={t('backToTools')}
onBack={goBack} onBack={() => (layoutStore.activeSubPanelId = null)}
> >
<Panel isOpen={isParticipantsOpen}> <Panel isOpen={isParticipantsOpen}>
<ParticipantsList /> <ParticipantsList />
@@ -65,7 +65,7 @@ const ToolButton = ({
<div> <div>
<Text <Text
margin={false} margin={false}
as="h2" as="h3"
className={css({ className={css({
display: 'flex', display: 'flex',
gap: 0.25, gap: 0.25,
@@ -11,7 +11,11 @@ import {
TIMEOUT_TICK, TIMEOUT_TICK,
timerWorkerScript, timerWorkerScript,
} from './TimerWorker' } from './TimerWorker'
import { BackgroundProcessorInterface, ProcessorConfig, ProcessorType } from '.' import {
BackgroundProcessorInterface,
BackgroundOptions,
ProcessorType,
} from '.'
const PROCESSING_WIDTH = 256 const PROCESSING_WIDTH = 256
const PROCESSING_HEIGHT = 144 const PROCESSING_HEIGHT = 144
@@ -29,7 +33,7 @@ const DEFAULT_BLUR = '10'
* MediaStreamTrackProcessor. * MediaStreamTrackProcessor.
*/ */
export class BackgroundCustomProcessor implements BackgroundProcessorInterface { export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
options: ProcessorConfig options: BackgroundOptions
name: string name: string
processedTrack?: MediaStreamTrack | undefined processedTrack?: MediaStreamTrack | undefined
@@ -60,10 +64,15 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
type: ProcessorType type: ProcessorType
virtualBackgroundImage?: HTMLImageElement virtualBackgroundImage?: HTMLImageElement
constructor(opts: ProcessorConfig) { constructor(opts: BackgroundOptions) {
this.name = 'blur' this.name = 'blur'
this.options = opts this.options = opts
this.type = opts.type
if (this.options.blurRadius) {
this.type = ProcessorType.BLUR
} else {
this.type = ProcessorType.VIRTUAL
}
} }
static get isSupported() { static get isSupported() {
@@ -98,12 +107,6 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
} }
_initVirtualBackgroundImage() { _initVirtualBackgroundImage() {
if (this.options.type !== 'virtual') {
throw new Error(
'Virtual background is only supported for virtual background'
)
}
const needsUpdate = const needsUpdate =
this.options.imagePath && this.options.imagePath &&
this.virtualBackgroundImage && this.virtualBackgroundImage &&
@@ -115,7 +118,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
} }
} }
async update(opts: ProcessorConfig): Promise<void> { async update(opts: BackgroundOptions): Promise<void> {
this.options = opts this.options = opts
this._initVirtualBackgroundImage() this._initVirtualBackgroundImage()
} }
@@ -184,7 +187,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
0, 0,
0, 0,
PROCESSING_WIDTH, PROCESSING_WIDTH,
PROCESSING_HEIGHT PROCESSING_WIDTH
) )
} }
@@ -209,9 +212,6 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
* TODO: future improvement with WebGL. * TODO: future improvement with WebGL.
*/ */
async blur() { async blur() {
if (this.options.type !== 'blur') {
throw new Error('Blurring is only supported for blur background')
}
const mask = this.imageSegmenterResult!.categoryMask!.getAsUint8Array() const mask = this.imageSegmenterResult!.categoryMask!.getAsUint8Array()
for (let i = 0; i < mask.length; ++i) { for (let i = 0; i < mask.length; ++i) {
this.segmentationMask!.data[i * 4 + 3] = 255 - mask[i] this.segmentationMask!.data[i * 4 + 3] = 255 - mask[i]
@@ -293,7 +293,7 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
await this.sizeSource() await this.sizeSource()
await this.segment() await this.segment()
if (this.options.type === 'blur') { if (this.options.blurRadius) {
await this.blur() await this.blur()
} else { } else {
await this.drawVirtualBackground() await this.drawVirtualBackground()
@@ -354,4 +354,15 @@ export class BackgroundCustomProcessor implements BackgroundProcessorInterface {
this.timerWorker?.terminate() this.timerWorker?.terminate()
this.imageSegmenter?.close() this.imageSegmenter?.close()
} }
clone() {
return new BackgroundCustomProcessor(this.options)
}
serialize() {
return {
type: this.type,
options: this.options,
}
}
} }
@@ -332,4 +332,15 @@ export class FaceLandmarksProcessor implements TrackProcessor<Track.Kind> {
this.timerWorker?.terminate() this.timerWorker?.terminate()
this.faceLandmarker?.close() this.faceLandmarker?.close()
} }
clone() {
return new FaceLandmarksProcessor(this.options)
}
serialize() {
return {
type: this.type,
options: this.options,
}
}
} }
@@ -1,23 +1,28 @@
import { ProcessorOptions, Track } from 'livekit-client' import { ProcessorOptions, Track } from 'livekit-client'
import { import {
BackgroundBlur, BackgroundBlur,
BackgroundTransformer,
ProcessorWrapper, ProcessorWrapper,
VirtualBackground, VirtualBackground,
} from '@livekit/track-processors' } from '@livekit/track-processors'
import { ProcessorConfig, BackgroundProcessorInterface, ProcessorType } from '.' import {
BackgroundOptions,
BackgroundProcessorInterface,
ProcessorType,
} from '.'
export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface { export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInterface {
processor: ProcessorWrapper<{ imagePath?: string; blurRadius?: number }> processor: ProcessorWrapper<BackgroundOptions>
opts: ProcessorConfig opts: BackgroundOptions
processorType: ProcessorType processorType: ProcessorType
constructor(opts: ProcessorConfig) { constructor(opts: BackgroundOptions) {
this.opts = opts this.opts = opts
if (opts.type === 'virtual') { if (opts.imagePath) {
this.processorType = ProcessorType.VIRTUAL this.processorType = ProcessorType.VIRTUAL
this.processor = VirtualBackground(opts.imagePath) this.processor = VirtualBackground(opts.imagePath)
} else if (opts.type === 'blur') { } else if (opts.blurRadius !== undefined) {
this.processorType = ProcessorType.BLUR this.processorType = ProcessorType.BLUR
this.processor = BackgroundBlur(opts.blurRadius) this.processor = BackgroundBlur(opts.blurRadius)
} else { } else {
@@ -39,23 +44,24 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
return this.processor.destroy() return this.processor.destroy()
} }
async update(opts: ProcessorConfig): Promise<void> { async update(opts: BackgroundOptions): Promise<void> {
this.opts = opts const newProcessorType = opts.imagePath
? ProcessorType.VIRTUAL
const newProcessorType = : ProcessorType.BLUR
opts.type === 'virtual' ? ProcessorType.VIRTUAL : ProcessorType.BLUR
let processedOpts = opts
if (newProcessorType !== this.processorType) { if (newProcessorType !== this.processorType) {
this.processorType = newProcessorType this.processorType = newProcessorType
if (newProcessorType === ProcessorType.VIRTUAL) { if (newProcessorType === ProcessorType.VIRTUAL) {
this.processor.name = 'virtual-background' this.processor.name = 'virtual-background'
processedOpts = { ...opts, blurRadius: undefined }
} else { } else {
this.processor.name = 'background-blur' this.processor.name = 'background-blur'
processedOpts = { ...opts, imagePath: undefined }
} }
} }
await this.processor.updateTransformerOptions( await this.processor.updateTransformerOptions(processedOpts)
opts as { imagePath?: string; blurRadius?: number } this.opts = processedOpts
)
} }
get name() { get name() {
@@ -67,6 +73,17 @@ export class UnifiedBackgroundTrackProcessor implements BackgroundProcessorInter
} }
get options() { get options() {
return this.opts return (this.processor.transformer as BackgroundTransformer).options
}
clone() {
return new UnifiedBackgroundTrackProcessor(this.options || this.opts)
}
serialize() {
return {
type: this.processorType,
options: this.options,
}
} }
} }
@@ -2,7 +2,23 @@ import { ProcessorWrapper } from '@livekit/track-processors'
import { Track, TrackProcessor } from 'livekit-client' import { Track, TrackProcessor } from 'livekit-client'
import { BackgroundCustomProcessor } from './BackgroundCustomProcessor' import { BackgroundCustomProcessor } from './BackgroundCustomProcessor'
import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor' import { UnifiedBackgroundTrackProcessor } from './UnifiedBackgroundTrackProcessor'
import { FaceLandmarksOptions } from './FaceLandmarksProcessor'
export type BackgroundOptions = {
blurRadius?: number
imagePath?: string
}
export interface ProcessorSerialized {
type: ProcessorType
options: BackgroundOptions
}
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
update(opts: BackgroundOptions): Promise<void>
options: BackgroundOptions
clone(): BackgroundProcessorInterface
serialize(): ProcessorSerialized
}
export enum ProcessorType { export enum ProcessorType {
BLUR = 'blur', BLUR = 'blur',
@@ -10,16 +26,6 @@ export enum ProcessorType {
FACE_LANDMARKS = 'faceLandmarks', FACE_LANDMARKS = 'faceLandmarks',
} }
export type ProcessorConfig =
| { type: ProcessorType.BLUR; blurRadius: number }
| { type: ProcessorType.VIRTUAL; imagePath: string; fileId?: string }
| ({ type: ProcessorType.FACE_LANDMARKS } & FaceLandmarksOptions)
export interface BackgroundProcessorInterface extends TrackProcessor<Track.Kind> {
update(opts: ProcessorConfig): Promise<void>
options: ProcessorConfig
}
export class BackgroundProcessorFactory { export class BackgroundProcessorFactory {
static hasModernApiSupport() { static hasModernApiSupport() {
return ProcessorWrapper.hasModernApiSupport return ProcessorWrapper.hasModernApiSupport
@@ -30,27 +36,28 @@ export class BackgroundProcessorFactory {
} }
static getProcessor( static getProcessor(
config: ProcessorConfig type: ProcessorType,
opts: BackgroundOptions
): BackgroundProcessorInterface | undefined { ): BackgroundProcessorInterface | undefined {
const isBlur = config.type === ProcessorType.BLUR const isBlur = type === ProcessorType.BLUR
const isVirtual = config.type === ProcessorType.VIRTUAL const isVirtual = type === ProcessorType.VIRTUAL
if (!isBlur && !isVirtual) return undefined if (!isBlur && !isVirtual) return undefined
if (ProcessorWrapper.isSupported) { if (ProcessorWrapper.isSupported) {
return new UnifiedBackgroundTrackProcessor(config) return new UnifiedBackgroundTrackProcessor(opts)
} }
if (BackgroundCustomProcessor.isSupported) { if (BackgroundCustomProcessor.isSupported) {
return new BackgroundCustomProcessor(config) return new BackgroundCustomProcessor(opts)
} }
return undefined return undefined
} }
static fromProcessorConfig(data?: ProcessorConfig) { static deserializeProcessor(data?: ProcessorSerialized) {
if (data) { if (data?.type) {
return BackgroundProcessorFactory.getProcessor(data) return BackgroundProcessorFactory.getProcessor(data?.type, data?.options)
} }
return undefined return undefined
} }
@@ -37,7 +37,6 @@ export const ChatToggle = ({
aria-label={t(tooltipLabel)} aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)} tooltip={t(tooltipLabel)}
isSelected={isChatOpen} isSelected={isChatOpen}
aria-expanded={isChatOpen}
onPress={(e) => { onPress={(e) => {
toggleChat() toggleChat()
onPress?.(e) onPress?.(e)
@@ -69,10 +69,10 @@ const SelectDevicePermissions = <T extends string | number>({
iconComponent={iconComponent} iconComponent={iconComponent}
placeholder={items.length === 0 ? t('loading') : t('select')} placeholder={items.length === 0 ? t('loading') : t('select')}
selectedKey={selectedKey} selectedKey={selectedKey}
onSelectionChange={async (key) => { onSelectionChange={(key) => {
if (key === selectedKey) return if (key === selectedKey) return
await setActiveMediaDevice(key as string)
onSubmit?.(key as string) onSubmit?.(key as string)
setActiveMediaDevice(key as string)
}} }}
{...props} {...props}
/> />
@@ -82,8 +82,8 @@ export const VideoDeviceControl = ({
* *
* See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121 * See https://github.com/numerique-gouv/meet/pull/309#issuecomment-2622404121
*/ */
const processor = BackgroundProcessorFactory.fromProcessorConfig( const processor = BackgroundProcessorFactory.deserializeProcessor(
userChoices.processorConfig userChoices.processorSerialized
) )
const toggle = trackProps.toggle as ( const toggle = trackProps.toggle as (
@@ -27,7 +27,6 @@ export const InfoToggle = ({
aria-label={t(tooltipLabel)} aria-label={t(tooltipLabel)}
tooltip={t(tooltipLabel)} tooltip={t(tooltipLabel)}
isSelected={isInfoOpen} isSelected={isInfoOpen}
aria-expanded={isInfoOpen}
onPress={(e) => { onPress={(e) => {
toggleInfo() toggleInfo()
onPress?.(e) onPress?.(e)
@@ -2,11 +2,11 @@ import { RiImageCircleAiFill } from '@remixicon/react'
import { MenuItem } from 'react-aria-components' import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { menuRecipe } from '@/primitives/menuRecipe' import { menuRecipe } from '@/primitives/menuRecipe'
import { type SidePanelStore, useSidePanel } from '../../../hooks/useSidePanel' import { useSidePanel } from '../../../hooks/useSidePanel'
export const EffectsMenuItem = ({ store }: { store?: SidePanelStore }) => { export const EffectsMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' }) const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { toggleEffects } = useSidePanel(store) const { toggleEffects } = useSidePanel()
return ( return (
<MenuItem <MenuItem
@@ -7,7 +7,6 @@ import { EffectsMenuItem } from './EffectsMenuItem'
import { SupportMenuItem } from './SupportMenuItem' import { SupportMenuItem } from './SupportMenuItem'
import { TranscriptMenuItem } from './TranscriptMenuItem' import { TranscriptMenuItem } from './TranscriptMenuItem'
import { ScreenRecordingMenuItem } from './ScreenRecordingMenuItem' import { ScreenRecordingMenuItem } from './ScreenRecordingMenuItem'
import { PictureInPictureMenuItem } from './PictureInPictureMenuItem'
// @todo try refactoring it to use MenuList component // @todo try refactoring it to use MenuList component
export const OptionsMenuItems = () => { export const OptionsMenuItems = () => {
@@ -22,7 +21,6 @@ export const OptionsMenuItems = () => {
<TranscriptMenuItem /> <TranscriptMenuItem />
<ScreenRecordingMenuItem /> <ScreenRecordingMenuItem />
<FullScreenMenuItem /> <FullScreenMenuItem />
<PictureInPictureMenuItem />
<EffectsMenuItem /> <EffectsMenuItem />
</MenuSection> </MenuSection>
<Separator /> <Separator />
@@ -1,23 +0,0 @@
import { MenuItem } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { RiPictureInPicture2Line } from '@remixicon/react'
import { menuRecipe } from '@/primitives/menuRecipe'
import { useRoomPiP } from '@/features/pip/hooks/useRoomPiP'
export const PictureInPictureMenuItem = () => {
const { t } = useTranslation('rooms', { keyPrefix: 'options.items' })
const { isSupported, isOpen, toggle } = useRoomPiP()
// Hide the entry when the browser doesn't support Document PiP.
if (!isSupported) return null
return (
<MenuItem
onAction={toggle}
className={menuRecipe({ icon: true, variant: 'dark' }).item}
>
<RiPictureInPicture2Line size={20} />
{isOpen ? t('pictureInPicture.exit') : t('pictureInPicture.enter')}
</MenuItem>
)
}

Some files were not shown because too many files have changed in this diff Show More