Compare commits

..

3 Commits

Author SHA1 Message Date
lebaudantoine 9310bdf36a 🔧(helm) update ASR model name after switch to WhisperX
Correct Automatic Speech Recognition model naming configuration to reflect
the transition from insanely-fast-whisper to WhisperX implementation.
2025-05-16 19:17:45 +02:00
lebaudantoine 21ed460783 🌐(backend) improve French translations for technical terminology
Refine French translations to be less literal and preserve English terms for
standard technical concepts. Enhances clarity and maintains industry
terminology conventions.
2025-05-16 19:16:01 +02:00
lebaudantoine d562a1fcae 🌐(backend) add missing translations for room PIN functionality
Add internationalization support for previously untranslated strings related
to room PIN code logic. Ensures consistent localization across all user-
facing room access features.
2025-05-16 19:11:51 +02:00
241 changed files with 4265 additions and 7158 deletions
+35
View File
@@ -0,0 +1,35 @@
name: Deploy
on:
push:
tags:
- 'preprod'
- 'production'
jobs:
notify-argocd:
runs-on: ubuntu-latest
steps:
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Call argocd github webhook
run: |
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${{ secrets.ARGOCD_WEBHOOK_SECRET }}'' | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_WEBHOOK_URL }}
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${{ secrets.ARGOCD_PRODUCTION_WEBHOOK_SECRET }}'' | awk '{print "X-Hub-Signature: sha1="$2}')
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ secrets.ARGOCD_PRODUCTION_WEBHOOK_URL }}
start-test-on-preprod:
needs:
- notify-argocd
runs-on: ubuntu-latest
if: startsWith(github.event.ref, 'refs/tags/preprod')
steps:
-
name: Debug
run: |
echo "Start test when preprod is ready"
+2 -37
View File
@@ -49,7 +49,7 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-frontend-generic:
build-and-push-frontend:
runs-on: ubuntu-latest
steps:
-
@@ -83,40 +83,6 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-frontend-dinum:
runs-on: ubuntu-latest
steps:
-
name: Checkout repository
uses: actions/checkout@v4
-
name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: lasuite/meet-frontend-dinum
-
name: Login to DockerHub
if: github.event_name != 'pull_request'
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
-
name: Run trivy scan
uses: numerique-gouv/action-trivy-cache@main
with:
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
docker-image-name: 'docker.io/lasuite/meet-frontend-dinum:${{ github.sha }}'
-
name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dinum-frontend/Dockerfile
target: frontend-production
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-and-push-summary:
runs-on: ubuntu-latest
steps:
@@ -147,8 +113,7 @@ jobs:
notify-argocd:
needs:
- build-and-push-frontend-generic
- build-and-push-frontend-dinum
- build-and-push-frontend
- build-and-push-backend
- build-and-push-summary
runs-on: ubuntu-latest
+66 -2
View File
@@ -81,7 +81,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
python-version: "3.12"
- name: Install development dependencies
run: pip install --user .[dev]
- name: Check code formatting with ruff
@@ -185,7 +185,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
python-version: "3.12"
- name: Install development dependencies
run: pip install --user .[dev]
@@ -249,3 +249,67 @@ jobs:
- name: Build SDK
run: npm run build
i18n-crowdin:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "infrastructure,secrets"
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
- name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/meet/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
- name: Install gettext (required to make messages)
run: |
sudo apt-get update
sudo apt-get install -y gettext
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install development dependencies
working-directory: src/backend
run: pip install --user .[dev]
- name: Generate the translation base file
run: ~/.local/bin/django-admin makemessages --keep-pot --all
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18.x"
cache: "npm"
cache-dependency-path: src/frontend/package-lock.json
- name: Install dependencies
run: cd src/frontend/ && npm ci
- name: Extract the frontend translation
run: make frontend-i18n-extract
- name: Upload files to Crowdin
uses: crowdin/github-action@v2
with:
config: crowdin/config.yml
upload_sources: true
upload_translations: true
download_translations: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_BASE_PATH: ${{ github.workspace }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
-2
View File
@@ -8,5 +8,3 @@ and this project adheres to
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
- 🔧(backend) support `_FILE` for secret environment variables #566
+4 -4
View File
@@ -1,7 +1,7 @@
# Django Meet
# ---- base image to inherit from ----
FROM python:3.13.5-alpine3.21 AS base
FROM python:3.12.6-alpine3.20 AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip setuptools
@@ -62,11 +62,11 @@ FROM base AS core
ENV PYTHONUNBUFFERED=1
RUN apk --no-cache add \
cairo \
gdk-pixbuf \
RUN apk add \
gettext \
cairo \
libffi-dev \
gdk-pixbuf \
pango \
shared-mime-info
-4
View File
@@ -347,7 +347,3 @@ start-tilt: ## start the kubernetes cluster using kind
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
DEV_ENV=dev-keycloak tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
start-tilt-dinum: ## start the kubernetes cluster using kind, without Pro Connect for authentication, but with DINUM styles
DEV_ENV=dev-dinum tilt up -f ./bin/Tiltfile
.PHONY: build-k8s-cluster
+2 -3
View File
@@ -33,9 +33,9 @@ Powered by [LiveKit](https://livekit.io/), La Suite Meet offers Zoom-level perfo
- Support for multiple screen sharing streams
- Non-persistent, secure chat
- End-to-end encryption (coming soon)
- Meeting recording
- Meeting recording (coming soon)
- Meeting transcription (currently in beta)
- Telephony integration
- Telephony integration (in development)
- Secure participation with robust authentication and access control
- LiveKit Advances features including :
- speaker detection
@@ -111,7 +111,6 @@ Come help us make La Suite Meet even better. We're growing fast and [would love
## Credits
We're using the awesome [LiveKit](https://livekit.io/) implementation. We're also thankful to the teams behind [Django Rest Framework](https://www.django-rest-framework.org/), [Vite.js](https://vite.dev/), and [React Aria](https://github.com/adobe/react-spectrum) — Thanks for your amazing work!
This project is tested with BrowserStack.
## License
+2 -23
View File
@@ -2,15 +2,6 @@ load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
load('ext://namespace', 'namespace_create', 'namespace_inject')
namespace_create('meet')
DEV_ENV = os.getenv('DEV_ENV', 'dev')
if DEV_ENV == 'dev-dinum':
update_settings(suppress_unused_image_warnings=["localhost:5001/meet-frontend-generic:latest"])
if DEV_ENV == 'dev-keycloak':
update_settings(suppress_unused_image_warnings=["localhost:5001/meet-frontend-dinum:latest"])
def clean_old_images(image_name):
local('docker images -q %s | tail -n +2 | xargs -r docker rmi' % image_name)
@@ -31,19 +22,7 @@ docker_build(
clean_old_images('localhost:5001/meet-backend')
docker_build(
'localhost:5001/meet-frontend-dinum:latest',
context='..',
dockerfile='../docker/dinum-frontend/Dockerfile',
only=['./src/frontend', './docker', './.dockerignore'],
target = 'frontend-production',
live_update=[
sync('../src/frontend', '/home/frontend'),
]
)
clean_old_images('localhost:5001/meet-frontend-dinum')
docker_build(
'localhost:5001/meet-frontend-generic:latest',
'localhost:5001/meet-frontend:latest',
context='..',
dockerfile='../src/frontend/Dockerfile',
only=['./src/frontend', './docker', './.dockerignore'],
@@ -52,7 +31,7 @@ docker_build(
sync('../src/frontend', '/home/frontend'),
]
)
clean_old_images('localhost:5001/meet-frontend-generic')
clean_old_images('localhost:5001/meet-frontend')
docker_build(
'localhost:5001/meet-summary:latest',
-1
View File
@@ -138,7 +138,6 @@ services:
target: frontend-production
args:
VITE_API_BASE_URL: "http://localhost:8071"
VITE_APP_TITLE: "LaSuite Meet"
image: meet:frontend-development
ports:
- "3000:8080"
-62
View File
@@ -1,62 +0,0 @@
# ---- Front-end image ----
FROM node:20-alpine AS frontend-deps
WORKDIR /home/frontend/
COPY ./src/frontend/package.json ./package.json
COPY ./src/frontend/package-lock.json ./package-lock.json
RUN npm ci
COPY .dockerignore ./.dockerignore
COPY ./src/frontend/ .
# ---- Front-end builder image ----
FROM frontend-deps AS meet-builder
WORKDIR /home/frontend
ENV VITE_APP_TITLE="Visio"
ENV VITE_BUILD_SOURCEMAP="true"
RUN npm run build
# Inject PostHog sourcemap metadata into the built assets
# This metadata is essential for correctly mapping errors to source maps in production
RUN set -e && \
npx @posthog/cli sourcemap inject --directory ./dist/assets
COPY ./docker/dinum-frontend/dinum-styles.css \
./dist/assets/
COPY ./docker/dinum-frontend/logo.svg \
./dist/assets/logo.svg
COPY ./docker/dinum-frontend/assets/ \
./dist/assets/
COPY ./docker/dinum-frontend/fonts/ \
./dist/assets/fonts/
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
USER root
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2
USER nginx
# Un-privileged user running the application
ARG DOCKER_USER
USER ${DOCKER_USER}
COPY --from=meet-builder \
/home/frontend/dist \
/usr/share/nginx/html
COPY ./src/frontend/default.conf /etc/nginx/conf.d
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
CMD ["nginx", "-g", "daemon off;"]
@@ -1,61 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="38.194309mm"
height="3.4877367mm"
viewBox="0 0 38.194309 3.4877367"
version="1.1"
id="svg1"
sodipodi:docname="gouvernement.svg"
inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="6.1918409"
inkscape:cx="79.620909"
inkscape:cy="44.332534"
inkscape:window-width="1901"
inkscape:window-height="1037"
inkscape:window-x="5"
inkscape:window-y="5"
inkscape:window-maximized="0"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<rect
x="111.07374"
y="333.22122"
width="541.4845"
height="297.51895"
id="rect2" />
<rect
x="158.10136"
y="358.3631"
width="425.55618"
height="204.21426"
id="rect1" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-53.656101,-100.08408)">
<path
d="m 55.435527,103.08144 c -0.72898,0 -1.249045,-0.56007 -1.249045,-1.25349 0,-0.69342 0.502285,-1.25349 1.204595,-1.25349 0.41783,0 0.760095,0.20447 0.96901,0.50673 l 0.333375,-0.25781 c -0.28448,-0.38227 -0.742315,-0.64008 -1.302385,-0.64008 -0.973455,0 -1.63576,0.75565 -1.63576,1.64465 0,0.889 0.680085,1.64465 1.68021,1.64465 0.582295,0 1.05791,-0.24892 1.346835,-0.64008 v -1.21349 h -1.31572 v 0.36005 h 0.89789 v 0.70231 c -0.217805,0.24447 -0.54229,0.40005 -0.929005,0.40005 z m 3.569334,-2.89814 c -0.973455,0 -1.63576,0.75565 -1.63576,1.64465 0,0.889 0.662305,1.64465 1.63576,1.64465 0.96901,0 1.631315,-0.75565 1.631315,-1.64465 0,-0.889 -0.662305,-1.64465 -1.631315,-1.64465 z m 0,2.89814 c -0.70231,0 -1.204595,-0.56007 -1.204595,-1.25349 0,-0.69342 0.502285,-1.25349 1.204595,-1.25349 0.697865,0 1.20015,0.56007 1.20015,1.25349 0,0.69342 -0.502285,1.25349 -1.20015,1.25349 z m 4.276088,-0.84455 c 0,0.53784 -0.31115,0.84455 -0.786765,0.84455 -0.48006,0 -0.79121,-0.30671 -0.79121,-0.84455 v -1.96469 h -0.41783 v 1.93802 c 0,0.8001 0.48006,1.26238 1.20904,1.26238 0.72898,0 1.204595,-0.46228 1.204595,-1.26238 v -1.93802 h -0.41783 z m 0.942341,-1.96469 1.2446,3.1115 h 0.55118 l 1.2446,-3.1115 h -0.4445 l -1.07569,2.68922 -1.07569,-2.68922 z m 3.653795,3.1115 h 1.67132 v -0.3556 h -1.25349 v -1.05347 h 1.07569 v -0.35115 h -1.07569 v -0.99568 h 1.25349 v -0.3556 h -1.67132 z m 2.502533,0 h 0.41783 v -1.36462 h 0.40894 c 0.03111,0 0.06667,0 0.09779,-0.004 l 0.902335,1.36906 h 0.493395 l -1.00457,-1.44907 c 0.333375,-0.13335 0.52451,-0.41339 0.52451,-0.78677 0,-0.5334 -0.386715,-0.87566 -1.01346,-0.87566 h -0.82677 z m 0.84455,-2.7559 c 0.3556,0 0.564515,0.19558 0.564515,0.51117 0,0.33338 -0.208915,0.52451 -0.564515,0.52451 h -0.42672 v -1.03568 z m 2.004699,2.7559 h 0.41783 v -2.60033 l 1.76022,2.60033 h 0.546735 v -3.1115 h -0.41783 v 2.60032 l -1.76022,-2.60032 h -0.546735 z m 3.71158,0 h 1.67132 v -0.3556 h -1.25349 v -1.05347 h 1.07569 v -0.35115 h -1.07569 v -0.99568 h 1.25349 v -0.3556 h -1.67132 z m 2.502533,0 h 0.41783 v -2.55143 l 0.90678,1.48463 h 0.32004 l 0.90678,-1.48463 v 2.55143 h 0.41783 v -3.1115 h -0.5334 l -0.95123,1.56908 -0.95123,-1.56908 h -0.5334 z m 3.951605,0 h 1.67132 v -0.3556 h -1.25349 v -1.05347 h 1.07569 v -0.35115 h -1.07569 v -0.99568 h 1.25349 v -0.3556 h -1.67132 z m 2.502535,0 h 0.41783 v -2.60033 l 1.76022,2.60033 h 0.546735 v -3.1115 h -0.41783 v 2.60032 l -1.76022,-2.60032 H 85.89712 Z m 3.373758,-2.72923 h 1.03124 v 2.72923 h 0.41783 v -2.72923 h 1.03124 v -0.38227 h -2.48031 z"
id="text3"
style="font-size:4.445px;line-height:0.661464px;font-family:Marianne;-inkscape-font-specification:'Marianne, Normal';letter-spacing:0px;word-spacing:0px;display:inline;stroke:#000000;stroke-width:0.198437"
aria-label="GOUVERNEMENT" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.3 KiB

-70
View File
@@ -1,70 +0,0 @@
:root {
--fonts-sans: 'Marianne', ui-sans-serif, system-ui, sans-serif;
}
.Header-beforeLogo {
display: block;
}
.Header-beforeLogo::before {
content: '';
display: block;
background-image: url(/assets/marianne.svg);
background-position: 0 -0.046875rem;
background-size: 2.0625rem 0.84375rem;
height: 0.75rem;
margin-bottom: 0.1rem;
width: 2.0625rem;
}
.Header-beforeLogo::after {
content: '';
display: block;
background-image: url(/assets/gouvernement.svg), url(/assets/devise.svg);
background-repeat: no-repeat, no-repeat;
background-size: 108.8px 10px, 40px 29px;
background-position: 0 3px, 0 18.9px;
width: 108.8px;
height: 48px;
margin-top: 0.25rem;
}
@font-face {
font-family: 'Marianne';
src: url('/assets/fonts/Marianne-Regular-subset.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/assets/fonts/Marianne-Regular_Italic-subset.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/assets/fonts/Marianne-Medium-subset.woff2') format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/assets/fonts/Marianne-Bold-subset.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Marianne';
src: url('/assets/fonts/Marianne-ExtraBold-subset.woff2') format('woff2');
font-weight: 800;
font-style: normal;
font-display: swap;
}
+3
View File
@@ -35,6 +35,9 @@ backend:
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
REDIS_URL: redis://default:pass@redis-master:6379/1
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
LIVEKIT_API_SECRET: secret
+100 -128
View File
@@ -1,6 +1,6 @@
# Installation on a k8s cluster
This document is a step-by-step guide that describes how to install LaSuite Meet on a k8s cluster without AI features.
This document is a step-by-step guide that describes how to install Visio on a k8s cluster without AI features.
## Prerequisites for a kubernetes setup
@@ -114,7 +114,7 @@ Please remember that \*.127.0.0.1.nip.io will always resolve to 127.0.0.1, excep
### What will you use to authenticate your users ?
LaSuite Meet uses OIDC, so if you already have an OIDC provider, obtain the necessary information to use it. In the next step, we will see how to configure Django (and thus LaSuite Meet) to use it. If you do not have a provider, we will show you how to deploy a local Keycloak instance (this is not a production deployment, just a demo).
Visio uses OIDC, so if you already have an OIDC provider, obtain the necessary information to use it. In the next step, we will see how to configure Django (and thus Visio) to use it. If you do not have a provider, we will show you how to deploy a local Keycloak instance (this is not a production deployment, just a demo).
If you haven't run the script **bin/start-kind.sh**, you'll need to manually create the namespace by running the following command:
@@ -134,8 +134,6 @@ keycloak-0 1/1 Running 0 6m48s
keycloak-postgresql-0 1/1 Running 0 6m48s
```
In your OIDC provider, set LaSuite Meet's redirect URI to `https://.../api/v1.0/callback/` where `...` should be replaced with the domain name LaSuite Meet is hosted on.
From here the important information you will need are :
```
@@ -154,7 +152,7 @@ You can find these values in **examples/keycloak.values.yaml**
### Find livekit server connexion values
LaSuite Meet use livekit for streaming part so if you have a livekit provider, obtain the necessary information to use it. If you do not have a provider, you can install a livekit testing environment as follows:
Visio use livekit for streaming part so if you have a livekit provider, obtain the necessary information to use it. If you do not have a provider, you can install a livekit testing environment as follows:
Livekit need a redis (and meet too) so we will start by deploying a redis :
@@ -196,7 +194,7 @@ CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
### Find postgresql connexion values
LaSuite Meet uses a postgresql db as backend so if you have a provider, obtain the necessary information to use it. If you do not have, you can install a postgresql testing environment as follows:
Visio uses a postgresql db as backend so if you have a provider, obtain the necessary information to use it. If you do not have, you can install a postgresql testing environment as follows:
```
$ helm install postgresql oci://registry-1.docker.io/bitnamicharts/postgresql -f examples/postgresql.values.yaml
@@ -217,11 +215,14 @@ DB_NAME: meet
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
POSTGRES_DB: meet
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
```
## Deployment
Now you are ready to deploy LaSuite Meet without AI. AI required more dependencies (Openai-compliant API, LiveKit Egress, Cold storage and a docs deployment to push resumes). To deploy meet you need to provide all previous information to the helm chart.
Now you are ready to deploy Visio without AI. AI required more dependencies (Openai-compliant API, LiveKit Egress, Cold storage and a docs deployment to push resumes). To deploy meet you need to provide all previous information to the helm chart.
```
$ helm repo add meet https://suitenumerique.github.io/meet/
@@ -242,129 +243,100 @@ meet <none> meet.127.0.0.1.nip.io localhost 80, 44
meet-admin <none> meet.127.0.0.1.nip.io localhost 80, 443 52m
```
You can use LaSuite Meet on https://meet.127.0.0.1.nip.io from the local device. The provisioning user in keycloak is meet/meet.
You can use Visio on https://meet.127.0.0.1.nip.io from the local device. The provisioning user in keycloak is meet/meet.
## All options
These are the environmental options available on meet backend.
| Option | Description | default |
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DATA_DIR | Data directory location | /data |
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used for Django security | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
| DB_NAME | Name of the database | meet |
| DB_USER | User used to connect to database | dinum |
| DB_PASSWORD | Password used to connect to the database | pass |
| DB_HOST | Hostname of the database | localhost |
| DB_PORT | Port to connect to database | 5432 |
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
| AWS_S3_REGION_NAME | S3 region | |
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
| DJANGO_LANGUAGE_CODE | Default language | en-us |
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
| FRONTEND_ANALYTICS | Analytics information | {} |
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
| FRONTEND_TRANSCRIPT | Frontend transcription configuration, you can pass a beta form, with `form_beta_users` | {} |
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_HOST | Host of the email server | |
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
| EMAIL_BRAND_NAME | Email branding name | |
| EMAIL_SUPPORT_EMAIL | Support email address | |
| EMAIL_LOGO_IMG | Email logo image | |
| EMAIL_DOMAIN | Email domain | |
| EMAIL_APP_BASE_URL | Email app base URL | |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
| SENTRY_DSN | Sentry server DSN | |
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
| OIDC_RP_SCOPES | OIDC scopes | openid email |
| OIDC_USE_NONCE | Use nonce for OIDC | true |
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False |
| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 |
| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 |
| LOGIN_REDIRECT_URL | Login redirect URL | |
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
| LIVEKIT_API_KEY | LiveKit API key | |
| LIVEKIT_API_SECRET | LiveKit API secret | |
| LIVEKIT_API_URL | LiveKit API URL | |
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false |
| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false |
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
| RECORDING_ENABLE | Record meeting option | false |
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
| RECORDING_EVENT_PARSER_CLASS | Storage event engine for recording | core.recording.event.parsers.MinioParser |
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | Enable storage event authorization | true |
| RECORDING_STORAGE_EVENT_ENABLE | Enable recording storage events | false |
| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | |
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
| BREVO_API_KEY | Brevo API key for marketing emails | |
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
| Option | Description | default |
| ----------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DJANGO_ALLOWED_HOSTS | Host that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | | false |
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
| DB_NAME | name of the database | meet |
| DB_USER | user used to connect to database | dinum |
| DB_PASSWORD | password used to connect to the database | pass |
| DB_HOST | hostname of the database | localhost |
| DB_PORT | port to connect to database | 5432 |
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
| AWS_S3_ACCESS_KEY_ID | s3 access key | |
| AWS_S3_SECRET_ACCESS_KEY | s3 secret key | |
| AWS_S3_REGION_NAME | s3 region | |
| AWS_STORAGE_BUCKET_NAME | s3 bucket name | |
| DJANGO_LANGUAGE_CODE | Default language | en-us |
| REDIS_URL | redis endpoint | redis://redis:6379/1 |
| REQUEST_ENTRY_THROTTLE_RATES | throttle rates | 150/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | deploy check | False |
| FRONTEND_ANALYTICS | analytics information | {} |
| FRONTEND_SUPPORT | crisp frontend support | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | silence livekit debug | false |
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_HOST | host of the email server | |
| DJANGO_EMAIL_HOST_USER | user to connect to the email server | |
| DJANGO_EMAIL_HOST_PASSWORD | password to connect tto the email server | |
| DJANGO_EMAIL_PORT | por tot connect to the email server | |
| DJANGO_EMAIL_USE_TLS | enable tls on email connection | false |
| DJANGO_EMAIL_USE_SSL | enable tls on email connection | false |
| DJANGO_EMAIL_FROM | email from account | from@example.com |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all cors origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | origins to allow in string | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | origins to allow in regex | [] |
| SENTRY_DSN | sentry server | |
| DJANGO_CELERY_BROKER_URL | celery broker host | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker options | {} |
| OIDC_CREATE_USER | create oidc user if not exists | false |
| OIDC_VERIFY_SSL | verify ssl for oidc | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | fallback to email for identification | false |
| OIDC_RP_SIGN_ALGO | token verification algoritm used by oidc | RS256 |
| OIDC_RP_CLIENT_ID | oidc client | meet |
| OIDC_RP_CLIENT_SECRET | oidc client secret | |
| OIDC_OP_JWKS_ENDPOINT | oidc endpoint for JWKS | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | oidc endpoint for authorization | |
| OIDC_OP_TOKEN_ENDPOINT | oidc endpoint for token | |
| OIDC_OP_USER_ENDPOINT | oidc endpoint for user | |
| OIDC_OP_USER_ENDPOINT_FORMAT | oidc endpoint format (AUTO, JWT, JSON) | AUTO |
| OIDC_OP_LOGOUT_ENDPOINT | oidc endpoint for logout | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | extra parameters for oidc request | |
| OIDC_RP_SCOPES | oidc scopes | openid email |
| LOGIN_REDIRECT_URL | login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | login redurect url for failure | |
| LOGOUT_REDIRECT_URL | url to redirect to on logout | |
| OIDC_USE_NONCE | use nonce for oidc | true |
| OIDC_REDIRECT_REQUIRE_HTTPS | require https for oidc | false |
| OIDC_REDIRECT_ALLOWED_HOSTS | allowed redirect hosts for oidc | [] |
| OIDC_STORE_ID_TOKEN | store oidc ID token | true |
| ALLOW_LOGOUT_GET_METHOD | allow logout through get method | true |
| OIDC_REDIRECT_FIELD_NAME | direct field for oidc | returnTo |
| OIDC_USERINFO_FULLNAME_FIELDS | full name claim from OIDC token | ["given_name", "usual_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | shortname claim from OIDC token | given_name |
| OIDC_USERINFO_ESSENTIAL_CLAIMS | required claims from OIDC token | [] |
| LIVEKIT_API_KEY | livekit api key | |
| LIVEKIT_API_SECRET | livekit api secret | |
| LIVEKIT_API_URL | livekit api url | |
| RESOURCE_DEFAULT_ACCESS_LEVEL | default resource access level for rooms | public |
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
| RECORDING_ENABLE | record meeting option | false |
| RECORDING_OUTPUT_FOLDER | folder to store meetings | recordings |
| RECORDING_WORKER_CLASSES | worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
| RECORDING_EVENT_PARSER_CLASS | storage event engine for recording | core.recording.event.parsers.MinioParser |
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | enable storage event authorization | true |
| RECORDING_STORAGE_EVENT_ENABLE | enable recording storage events | false |
| RECORDING_STORAGE_EVENT_TOKEN | recording storage event token | |
| SUMMARY_SERVICE_ENDPOINT | summary service endpoint | |
| SUMMARY_SERVICE_API_TOKEN | api token voor summary service | |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | signup users to marketing emails | false |
| MARKETING_SERVICE_CLASS | markering class | core.services.marketing.BrevoMarketingService |
| BREVO_API_KEY | breva api key for marketing emails | |
| BREVO_API_CONTACT_LIST_IDS | brevo api contact list ids | [] |
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | brevo contact attributes | {"VISIO_USER": True} |
| BREVO_API_TIMEOUT | brevo timeout | 1 |
| LOBBY_KEY_PREFIX | lobby prefix | room_lobby |
| LOBBY_WAITING_TIMEOUT | lobby waiting tumeout | 3 |
| LOBBY_DENIED_TIMEOUT | lobby deny timeout | 5 |
| LOBBY_ACCEPTED_TIMEOUT | lobby accept timeout | 21600 |
| LOBBY_NOTIFICATION_TYPE | lobby notification types | participantWaiting |
| LOBBY_COOKIE_NAME | lobby cookie name | lobbyParticipantId |
+1 -4
View File
@@ -42,7 +42,7 @@ LOGIN_REDIRECT_URL=http://localhost:3000
LOGIN_REDIRECT_URL_FAILURE=http://localhost:3000
LOGOUT_REDIRECT_URL=http://localhost:3000
OIDC_REDIRECT_ALLOWED_HOSTS=localhost:8083,localhost:3000
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# Livekit Token settings
@@ -57,6 +57,3 @@ SCREEN_RECORDING_BASE_URL=http://localhost:3000/recordings
# Telephony
ROOM_TELEPHONY_ENABLED=True
FRONTEND_USE_FRENCH_GOV_FOOTER=False
FRONTEND_USE_PROCONNECT_BUTTON=False
+1 -1
View File
@@ -7,7 +7,7 @@
"enabled": false,
"groupName": "ignored python dependencies",
"matchManagers": ["pep621"],
"matchPackageNames": ["redis"]
"matchPackageNames": []
},
{
"enabled": false,
+1 -30
View File
@@ -99,7 +99,6 @@ class ResourceAccessInline(admin.TabularInline):
model = models.ResourceAccess
extra = 0
autocomplete_fields = ["user"]
@admin.register(models.Room)
@@ -107,10 +106,6 @@ class RoomAdmin(admin.ModelAdmin):
"""Room admin interface declaration."""
inlines = (ResourceAccessInline,)
search_fields = ["name", "slug", "=id"]
list_display = ["name", "slug", "access_level", "created_at"]
list_filter = ["access_level", "created_at"]
readonly_fields = ["id", "created_at", "updated_at"]
class RecordingAccessInline(admin.TabularInline):
@@ -125,28 +120,4 @@ class RecordingAdmin(admin.ModelAdmin):
"""Recording admin interface declaration."""
inlines = (RecordingAccessInline,)
search_fields = ["status", "=id", "worker_id", "room__slug", "=room__id"]
list_display = ("id", "status", "room", "get_owner", "created_at", "worker_id")
list_filter = ["status", "room", "created_at"]
readonly_fields = ["id", "created_at", "updated_at"]
def get_queryset(self, request):
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
return super().get_queryset(request).prefetch_related("accesses__user")
def get_owner(self, obj):
"""Return the owner of the recording for display in the admin list."""
owners = [
access
for access in obj.accesses.all()
if access.role == models.RoleChoices.OWNER
]
if not owners:
return _("No owner")
if len(owners) > 1:
return _("Multiple owners")
return str(owners[0].user)
list_display = ("id", "status", "room", "created_at", "worker_id")
-13
View File
@@ -41,19 +41,6 @@ def get_frontend_configuration(request):
"is_enabled": settings.RECORDING_ENABLE,
"available_modes": settings.RECORDING_WORKER_CLASSES.keys(),
"expiration_days": settings.RECORDING_EXPIRATION_DAYS,
"max_duration": settings.RECORDING_MAX_DURATION,
},
"telephony": {
"enabled": settings.ROOM_TELEPHONY_ENABLED,
"phone_number": settings.ROOM_TELEPHONY_PHONE_NUMBER
if settings.ROOM_TELEPHONY_ENABLED
else None,
"default_country": settings.ROOM_TELEPHONY_DEFAULT_COUNTRY,
},
"livekit": {
"url": settings.LIVEKIT_CONFIGURATION["url"],
"force_wss_protocol": settings.LIVEKIT_FORCE_WSS_PROTOCOL,
"enable_firefox_proxy_workaround": settings.LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND,
},
}
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
+3 -3
View File
@@ -64,7 +64,7 @@ class RoomPermissions(permissions.BasePermission):
if request.method == "DELETE":
return obj.is_owner(user)
return obj.is_administrator_or_owner(user)
return obj.is_administrator(user)
class ResourceAccessPermission(IsAuthenticated):
@@ -80,7 +80,7 @@ class ResourceAccessPermission(IsAuthenticated):
if request.method == "DELETE" and obj.role == RoleChoices.OWNER:
return obj.user == user
return obj.resource.is_administrator_or_owner(user)
return obj.resource.is_administrator(user)
class HasAbilityPermission(IsAuthenticated):
@@ -98,7 +98,7 @@ class HasPrivilegesOnRoom(IsAuthenticated):
def has_object_permission(self, request, view, obj):
"""Determine if user has privileges on room."""
return obj.is_administrator_or_owner(request.user)
return obj.is_owner(request.user) or obj.is_administrator(request.user)
class IsRecordingEnabled(permissions.BasePermission):
+9 -23
View File
@@ -1,7 +1,5 @@
"""Client serializers for the Meet core app."""
import uuid
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -39,10 +37,10 @@ class ResourceAccessSerializerMixin:
# Update
self.instance
and (
data.get("role") == models.RoleChoices.OWNER
data["role"] == models.RoleChoices.OWNER
and not self.instance.resource.is_owner(user)
or self.instance.role == models.RoleChoices.OWNER
and self.instance.user != user
and not self.instance.user == user
)
) or (
# Create
@@ -60,9 +58,7 @@ class ResourceAccessSerializerMixin:
request = self.context.get("request", None)
user = getattr(request, "user", None)
if not (
user and user.is_authenticated and resource.is_administrator_or_owner(user)
):
if not (user and user.is_authenticated and resource.is_administrator(user)):
raise PermissionDenied(
_("You must be administrator or owner of a room to add accesses to it.")
)
@@ -107,8 +103,8 @@ class RoomSerializer(serializers.ModelSerializer):
class Meta:
model = models.Room
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
read_only_fields = ["id", "slug", "pin_code"]
fields = ["id", "name", "slug", "configuration", "access_level"]
read_only_fields = ["id", "slug"]
def to_representation(self, instance):
"""
@@ -122,11 +118,9 @@ class RoomSerializer(serializers.ModelSerializer):
return output
role = instance.get_role(request.user)
is_admin_or_owner = models.RoleChoices.check_administrator_role(
role
) or models.RoleChoices.check_owner_role(role)
is_admin = models.RoleChoices.check_administrator_role(role)
if is_admin_or_owner:
if is_admin:
access_serializer = NestedResourceAccessSerializer(
instance.accesses.select_related("resource", "user").all(),
context=self.context,
@@ -134,7 +128,7 @@ class RoomSerializer(serializers.ModelSerializer):
)
output["accesses"] = access_serializer.data
if not is_admin_or_owner:
if not is_admin:
del output["configuration"]
should_access_room = (
@@ -153,7 +147,7 @@ class RoomSerializer(serializers.ModelSerializer):
room_id=room_id, user=request.user, username=username
)
output["is_administrable"] = is_admin_or_owner
output["is_administrable"] = is_admin
return output
@@ -221,14 +215,6 @@ class ParticipantEntrySerializer(serializers.Serializer):
participant_id = serializers.CharField(required=True)
allow_entry = serializers.BooleanField(required=True)
def validate_participant_id(self, value):
"""Validate that the participant_id is a valid UUID hex string."""
try:
uuid.UUID(hex=value, version=4)
except (ValueError, TypeError) as e:
raise serializers.ValidationError("Invalid UUID hex format") from e
return value
def create(self, validated_data):
"""Not implemented as this is a validation-only serializer."""
raise NotImplementedError("ParticipantEntrySerializer is validation-only")
+11
View File
@@ -0,0 +1,11 @@
"""Meet Core application"""
# from django.apps import AppConfig
# from django.utils.translation import gettext_lazy as _
# class CoreConfig(AppConfig):
# """Configuration class for the Meet core app."""
# name = "core"
# app_label = "core"
# verbose_name = _("meet core application")
+1
View File
@@ -1,3 +1,4 @@
# ruff: noqa: S311
"""
Core application factories
"""
@@ -17,7 +17,7 @@ def generate_pin_for_rooms(apps, schema_editor):
def generate_pin_code():
while True:
pin_code = str(secrets.randbelow(10 ** length)).zfill(length)
if pin_code not in existing_pins:
if not pin_code in existing_pins:
return pin_code
for room in rooms_without_pin_code:
+7 -7
View File
@@ -35,7 +35,7 @@ class RoleChoices(models.TextChoices):
@classmethod
def check_administrator_role(cls, role):
"""Check if a role is administrator."""
return role == cls.ADMIN
return role in [cls.ADMIN, cls.OWNER]
@classmethod
def check_owner_role(cls, role):
@@ -288,13 +288,13 @@ class Resource(BaseModel):
role = RoleChoices.MEMBER
return role
def is_administrator_or_owner(self, user):
def is_administrator(self, user):
"""
Check if a user is administrator or owner of the resource."""
role = self.get_role(user)
return RoleChoices.check_administrator_role(
role
) or RoleChoices.check_owner_role(role)
Check if a user is administrator of the resource.
Users carrying the "owner" role are considered as administrators a fortiori.
"""
return RoleChoices.check_administrator_role(self.get_role(user))
def is_owner(self, user):
"""Check if a user is owner of the resource."""
@@ -136,13 +136,6 @@ class NotificationService:
"filename": recording.key,
"email": owner_access.user.email,
"sub": owner_access.user.sub,
"room": recording.room.name,
"recording_date": recording.created_at.astimezone(
owner_access.user.timezone
).strftime("%Y-%m-%d"),
"recording_time": recording.created_at.astimezone(
owner_access.user.timezone
).strftime("%H:%M"),
}
headers = {
+2 -2
View File
@@ -86,7 +86,7 @@ class MinioParser:
# pylint: disable=line-too-long
self._filepath_regex = re.compile(
r"(?P<url_encoded_folder_path>(?:[^%]+%2F)+)?(?P<recording_id>[0-9a-fA-F\-]{36})\.(?P<extension>[a-zA-Z0-9]+)"
r"(?P<url_encoded_folder_path>(?:[^%]+%2F)*)?(?P<recording_id>[0-9a-fA-F\-]{36})\.(?P<extension>[a-zA-Z0-9]+)"
)
@staticmethod
@@ -123,7 +123,7 @@ class MinioParser:
f"Invalid bucket: expected {self._bucket_name}, got {event_data.bucket_name}"
)
if event_data.filetype not in self._allowed_filetypes:
if not event_data.filetype in self._allowed_filetypes:
raise InvalidFileTypeError(
f"Invalid file type, expected {self._allowed_filetypes},"
f"got '{event_data.filetype}'"
@@ -1,49 +0,0 @@
"""Recording-related LiveKit Events Service"""
from logging import getLogger
from core import models, utils
logger = getLogger(__name__)
class RecordingEventsError(Exception):
"""Recording event handling fails."""
class RecordingEventsService:
"""Handles recording-related Livekit webhook events."""
@staticmethod
def handle_limit_reached(recording):
"""Stop recording and notify participants when limit is reached."""
recording.status = models.RecordingStatusChoices.STOPPED
recording.save()
notification_mapping = {
models.RecordingModeChoices.SCREEN_RECORDING: "screenRecordingLimitReached",
models.RecordingModeChoices.TRANSCRIPT: "transcriptionLimitReached",
}
notification_type = notification_mapping.get(recording.mode)
if not notification_type:
return
try:
utils.notify_participants(
room_name=str(recording.room.id),
notification_data={"type": notification_type},
)
except utils.NotificationError as e:
logger.exception(
"Failed to notify participants about recording limit reached: "
"room=%s, recording_id=%s, mode=%s",
recording.room.id,
recording.id,
recording.mode,
)
raise RecordingEventsError(
f"Failed to notify participants in room '{recording.room.id}' about "
f"recording limit reached (recording_id={recording.id})"
) from e
+1 -82
View File
@@ -1,25 +1,13 @@
"""LiveKit Events Service"""
# pylint: disable=E1101
import uuid
from enum import Enum
from logging import getLogger
from django.conf import settings
from livekit import api
from core import models
from core.recording.services.recording_events import (
RecordingEventsError,
RecordingEventsService,
)
from .lobby import LobbyService
from .telephony import TelephonyException, TelephonyService
logger = getLogger(__name__)
class LiveKitWebhookError(Exception):
@@ -89,8 +77,6 @@ class LiveKitEventsService:
)
self.webhook_receiver = api.WebhookReceiver(token_verifier)
self.lobby_service = LobbyService()
self.telephony_service = TelephonyService()
self.recording_events = RecordingEventsService()
def receive(self, request):
"""Process webhook and route to appropriate handler."""
@@ -122,77 +108,10 @@ class LiveKitEventsService:
# pylint: disable=not-callable
handler(data)
def _handle_egress_ended(self, data):
"""Handle 'egress_ended' event."""
try:
recording = models.Recording.objects.get(
worker_id=data.egress_info.egress_id
)
except models.Recording.DoesNotExist as err:
raise ActionFailedError(
f"Recording with worker ID {data.egress_info.egress_id} does not exist"
) from err
if (
data.egress_info.status == api.EgressStatus.EGRESS_LIMIT_REACHED
and recording.status == models.RecordingStatusChoices.ACTIVE
):
try:
self.recording_events.handle_limit_reached(recording)
except RecordingEventsError as e:
raise ActionFailedError(
f"Failed to process limit reached event for recording {recording}"
) from e
def _handle_room_started(self, data):
"""Handle 'room_started' event."""
try:
room_id = uuid.UUID(data.room.name)
except ValueError as e:
logger.warning(
"Ignoring room event: room name '%s' is not a valid UUID format.",
data.room.name,
)
raise ActionFailedError("Failed to process room started event") from e
try:
room = models.Room.objects.get(id=room_id)
except models.Room.DoesNotExist as err:
raise ActionFailedError(f"Room with ID {room_id} does not exist") from err
if settings.ROOM_TELEPHONY_ENABLED:
try:
self.telephony_service.create_dispatch_rule(room)
except TelephonyException as e:
raise ActionFailedError(
f"Failed to create telephony dispatch rule for room {room_id}"
) from e
def _handle_room_finished(self, data):
"""Handle 'room_finished' event."""
try:
room_id = uuid.UUID(data.room.name)
except ValueError as e:
logger.warning(
"Ignoring room event: room name '%s' is not a valid UUID format.",
data.room.name,
)
raise ActionFailedError("Failed to process room finished event") from e
if settings.ROOM_TELEPHONY_ENABLED:
try:
self.telephony_service.delete_dispatch_rule(room_id)
except TelephonyException as e:
raise ActionFailedError(
f"Failed to delete telephony dispatch rule for room {room_id}"
) from e
try:
self.lobby_service.clear_room_cache(room_id)
except Exception as e:
raise ActionFailedError(
f"Failed to clear room cache for room {room_id}"
) from e
raise ActionFailedError("Failed to process room finished event") from e
+56 -8
View File
@@ -1,5 +1,6 @@
"""Lobby Service"""
import json
import logging
import uuid
from dataclasses import dataclass
@@ -10,6 +11,13 @@ from uuid import UUID
from django.conf import settings
from django.core.cache import cache
from asgiref.sync import async_to_sync
from livekit.api import ( # pylint: disable=E0611
ListRoomsRequest,
SendDataRequest,
TwirpError,
)
from core import models, utils
logger = logging.getLogger(__name__)
@@ -38,6 +46,10 @@ class LobbyParticipantNotFound(LobbyError):
"""Raised when participant is not found."""
class LobbyNotificationError(LobbyError):
"""Raised when LiveKit notification fails."""
@dataclass
class LobbyParticipant:
"""Participant in a lobby system."""
@@ -199,6 +211,9 @@ class LobbyService:
Create a new participant entry in waiting status and notify room
participants of the new entry request.
Raises:
LobbyNotificationError: If room notification fails
"""
color = utils.generate_color(participant_id)
@@ -211,15 +226,10 @@ class LobbyService:
)
try:
utils.notify_participants(
room_name=str(room_id),
notification_data={
"type": settings.LOBBY_NOTIFICATION_TYPE,
},
)
except utils.NotificationError:
self.notify_participants(room_id=room_id)
except LobbyNotificationError:
# If room not created yet, there is no participants to notify
logger.exception("Failed to notify room participants")
pass
cache_key = self._get_cache_key(room_id, participant_id)
cache.set(
@@ -324,6 +334,44 @@ class LobbyService:
participant.status = status
cache.set(cache_key, participant.to_dict(), timeout=timeout)
@async_to_sync
async def notify_participants(self, room_id: UUID):
"""Notify room participants about a new waiting participant using LiveKit.
Raises:
LobbyNotificationError: If notification fails to send
"""
notification_data = {
"type": settings.LOBBY_NOTIFICATION_TYPE,
}
lkapi = utils.create_livekit_client()
try:
room_response = await lkapi.room.list_rooms(
ListRoomsRequest(
names=[str(room_id)],
)
)
# Check if the room exists
if not room_response.rooms:
return
await lkapi.room.send_data(
SendDataRequest(
room=str(room_id),
data=json.dumps(notification_data).encode("utf-8"),
kind="RELIABLE",
)
)
except TwirpError as e:
logger.exception("Failed to notify room participants")
raise LobbyNotificationError("Failed to notify room participants") from e
finally:
await lkapi.aclose()
def clear_room_cache(self, room_id: UUID) -> None:
"""Clear all participant entries from the cache for a specific room."""
+2 -6
View File
@@ -10,7 +10,6 @@ from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
import brevo_python
import urllib3
logger = logging.getLogger(__name__)
@@ -121,11 +120,8 @@ class BrevoMarketingService:
try:
response = contact_api.create_contact(contact, **api_configurations)
except (
brevo_python.rest.ApiException,
urllib3.exceptions.ReadTimeoutError,
) as err:
logger.warning("Failed to create contact in Brevo", exc_info=True)
except brevo_python.rest.ApiException as err:
logger.exception("Failed to create contact in Brevo")
raise ContactCreationError("Failed to create contact in Brevo") from err
return response
-124
View File
@@ -1,124 +0,0 @@
"""Telephony service for managing SIP dispatch rules for room access."""
from logging import getLogger
from asgiref.sync import async_to_sync
from livekit.api import TwirpError
from livekit.protocol.sip import (
CreateSIPDispatchRuleRequest,
DeleteSIPDispatchRuleRequest,
ListSIPDispatchRuleRequest,
SIPDispatchRule,
SIPDispatchRuleDirect,
)
from core import utils
logger = getLogger(__name__)
class TelephonyException(Exception):
"""Exception raised when telephony operations fail."""
class TelephonyService:
"""Service for managing participant access through the telephony system (SIP)."""
def _rule_name(self, room_id):
"""Generate the rule name for a room based on its ID."""
return f"SIP_{str(room_id)}"
@async_to_sync
async def create_dispatch_rule(self, room):
"""Create a SIP inbound dispatch rule for direct room routing.
Configures telephony to route incoming SIP calls directly to the specified room
using the room's ID and PIN code for authentication.
"""
direct_rule = SIPDispatchRule(
dispatch_rule_direct=SIPDispatchRuleDirect(
room_name=str(room.id), pin=str(room.pin_code)
)
)
request = CreateSIPDispatchRuleRequest(
rule=direct_rule, name=self._rule_name(room.id)
)
lkapi = utils.create_livekit_client()
try:
await lkapi.sip.create_sip_dispatch_rule(create=request)
except TwirpError as e:
logger.exception(
"Unexpected error creating dispatch rule for room %s", room.id
)
raise TelephonyException("Could not create dispatch rule") from e
finally:
await lkapi.aclose()
async def _list_dispatch_rules_ids(self, room_id):
"""List SIP dispatch rule IDs for a specific room.
Fetches all existing SIP dispatch rules and filters them by room name
since LiveKit API doesn't support server-side filtering by 'room_name'.
This approach is acceptable for moderate scale but may need refactoring
for high-volume scenarios.
Note:
Feature request for server-side filtering: livekit/sip#405
"""
lkapi = utils.create_livekit_client()
try:
existing_rules = await lkapi.sip.list_sip_dispatch_rule(
list=ListSIPDispatchRuleRequest()
)
except TwirpError as e:
logger.exception("Failed to list dispatch rules for room %s", room_id)
raise TelephonyException("Could not list dispatch rules") from e
finally:
await lkapi.aclose()
if not existing_rules or not existing_rules.items:
return []
rule_name = self._rule_name(room_id)
return [
existing_rule.sip_dispatch_rule_id
for existing_rule in existing_rules.items
if existing_rule.name == rule_name
]
@async_to_sync
async def delete_dispatch_rule(self, room_id):
"""Delete all SIP inbound dispatch rules associated with a specific room."""
rules_ids = await self._list_dispatch_rules_ids(room_id)
if not rules_ids:
logger.info("No dispatch rules found for room %s", room_id)
return False
if len(rules_ids) > 1:
logger.error("Multiple dispatch rules found for room %s", room_id)
lkapi = utils.create_livekit_client()
try:
for rule_id in rules_ids:
await lkapi.sip.delete_sip_dispatch_rule(
delete=DeleteSIPDispatchRuleRequest(sip_dispatch_rule_id=rule_id)
)
return True
except TwirpError as e:
logger.exception("Failed to delete dispatch rules for room %s", room_id)
raise TelephonyException("Could not delete dispatch rules") from e
finally:
await lkapi.aclose()
@@ -287,7 +287,9 @@ def test_finds_user_whitespace_email(django_assert_num_queries, settings):
[
"john.doe@xample.com", # Fullwidth character in domain
"john.doe@еxample.com", # Cyrillic 'е' in domain
"JOHN.DOe@exam𝔭le.com", # Mixed Gothic '𝔭' in domain
"john.doe@exаmple.com", # Cyrillic 'а' (a) in domain
"john.doe@e𝓧𝓪𝓶𝓹𝓵𝓮.com", # Mixed fullwidth and cursive in domain
],
)
def test_authentication_getter_existing_user_email_tricky(email, monkeypatch, settings):
@@ -129,11 +129,8 @@ def test_validate_invalid_filetype(minio_parser):
@pytest.mark.parametrize(
"invalid_filepath",
[
"invalid_filepath", # totally invalid string
"invalid_filepath",
"recording/46d1a121-2426-484d-8fb3-09b5d886f7a8.ogg",
"recording/46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing extension
"46d1a121-2426-484d-8fb3-09b5d886f7a8", # missing url_encoded_folder_path and extension
"", # empty string
"recording%2F46d1a1212426484d8fb309b5d886f7a8.ogg",
],
)
@@ -1,72 +0,0 @@
"""
Test RecordingEventsService service.
"""
# pylint: disable=W0621
from unittest import mock
import pytest
from core.factories import RecordingFactory
from core.recording.services.recording_events import (
RecordingEventsError,
RecordingEventsService,
)
from core.utils import NotificationError
pytestmark = pytest.mark.django_db
@pytest.fixture
def service():
"""Initialize RecordingEventsService."""
return RecordingEventsService()
@pytest.mark.parametrize(
("mode", "notification_type"),
(
("screen_recording", "screenRecordingLimitReached"),
("transcript", "transcriptionLimitReached"),
),
)
@mock.patch("core.utils.notify_participants")
def test_handle_limit_reached_success(mock_notify, mode, notification_type, service):
"""Test handle_limit_reached stops recording and notifies participants."""
recording = RecordingFactory(status="active", mode=mode)
service.handle_limit_reached(recording)
assert recording.status == "stopped"
mock_notify.assert_called_once_with(
room_name=str(recording.room.id), notification_data={"type": notification_type}
)
@pytest.mark.parametrize(
("mode", "notification_type"),
(
("screen_recording", "screenRecordingLimitReached"),
("transcript", "transcriptionLimitReached"),
),
)
@mock.patch("core.utils.notify_participants")
def test_handle_limit_reached_error(mock_notify, mode, notification_type, service):
"""Test handle_limit_reached raises RecordingEventsError when notification fails."""
mock_notify.side_effect = NotificationError("Error notifying")
recording = RecordingFactory(status="active", mode=mode)
with pytest.raises(
RecordingEventsError,
match=r"Failed to notify participants in room '.+' "
r"about recording limit reached \(recording_id=.+\)",
):
service.handle_limit_reached(recording)
assert recording.status == "stopped"
mock_notify.assert_called_once_with(
room_name=str(recording.room.id), notification_data={"type": notification_type}
)
@@ -230,14 +230,15 @@ def test_api_rooms_invite_error(mock_invite_to_room):
data = {"emails": ["toto@yopmail.com", "toto@yopmail.com"]}
with pytest.raises(InvitationError):
with pytest.raises(InvitationError) as excinfo:
client.post(
f"/api/v1.0/rooms/{room.id}/invite/",
json.dumps(data),
content_type="application/json",
)
mock_invite_to_room.assert_called_once()
mock_invite_to_room.assert_called_once()
assert "Could not send invitation" in str(excinfo.value)
@mock.patch("core.services.invitation.send_mail")
@@ -37,7 +37,7 @@ def test_request_entry_anonymous(settings):
assert not lobby_keys
with (
mock.patch.object(utils, "notify_participants", return_value=None),
mock.patch.object(LobbyService, "notify_participants", return_value=None),
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
):
response = client.post(
@@ -86,7 +86,7 @@ def test_request_entry_authenticated_user(settings):
assert not lobby_keys
with (
mock.patch.object(utils, "notify_participants", return_value=None),
mock.patch.object(LobbyService, "notify_participants", return_value=None),
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
):
response = client.post(
@@ -132,18 +132,18 @@ def test_request_entry_with_existing_participants(settings):
# Add two participants already waiting in the lobby
cache.set(
f"mocked-cache-prefix_{room.id}_2f7f162fe7d1421b90e702bfbfbf8def",
f"mocked-cache-prefix_{room.id}_participant1",
{
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "participant1",
"username": "user1",
"status": "waiting",
"color": "#123456",
},
)
cache.set(
f"mocked-cache-prefix_{room.id}_f4ca3ab8a6c04ad88097b8da33f60f10",
f"mocked-cache-prefix_{room.id}_participant2",
{
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
"id": "participant2",
"username": "user2",
"status": "accepted",
"color": "#654321",
@@ -156,7 +156,7 @@ def test_request_entry_with_existing_participants(settings):
# Mock external service calls to isolate the test
with (
mock.patch.object(utils, "notify_participants", return_value=None),
mock.patch.object(LobbyService, "notify_participants", return_value=None),
mock.patch.object(utils, "generate_color", return_value="mocked-color"),
):
# Make request as a new anonymous user
@@ -205,7 +205,7 @@ def test_request_entry_public_room(settings):
assert not lobby_keys
with (
mock.patch.object(utils, "notify_participants", return_value=None),
mock.patch.object(LobbyService, "notify_participants", return_value=None),
mock.patch.object(
LobbyService, "_get_or_create_participant_id", return_value="123"
),
@@ -255,11 +255,9 @@ def test_request_entry_authenticated_user_public_room(settings):
assert not lobby_keys
with (
mock.patch.object(utils, "notify_participants", return_value=None),
mock.patch.object(LobbyService, "notify_participants", return_value=None),
mock.patch.object(
LobbyService,
"_get_or_create_participant_id",
return_value="2f7f162fe7d1421b90e702bfbfbf8def",
LobbyService, "_get_or_create_participant_id", return_value="123"
),
mock.patch.object(
utils, "generate_livekit_config", return_value={"token": "test-token"}
@@ -276,11 +274,11 @@ def test_request_entry_authenticated_user_public_room(settings):
# Verify the lobby cookie was set
cookie = response.cookies.get("mocked-cookie")
assert cookie is not None
assert cookie.value == "2f7f162fe7d1421b90e702bfbfbf8def"
assert cookie.value == "123"
# Verify response content matches expected structure and values
assert response.json() == {
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "123",
"username": "test_user",
"status": "accepted",
"color": "mocked-color",
@@ -302,9 +300,9 @@ def test_request_entry_waiting_participant_public_room(settings):
# Add a waiting participant to the room's lobby cache
cache.set(
f"mocked-cache-prefix_{room.id}_2f7f162fe7d1421b90e702bfbfbf8def",
f"mocked-cache-prefix_{room.id}_participant1",
{
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "participant1",
"username": "user1",
"status": "waiting",
"color": "#123456",
@@ -312,10 +310,10 @@ def test_request_entry_waiting_participant_public_room(settings):
)
# Simulate a browser with existing participant cookie
client.cookies.load({"mocked-cookie": "2f7f162fe7d1421b90e702bfbfbf8def"})
client.cookies.load({"mocked-cookie": "participant1"})
with (
mock.patch.object(utils, "notify_participants", return_value=None),
mock.patch.object(LobbyService, "notify_participants", return_value=None),
mock.patch.object(
utils, "generate_livekit_config", return_value={"token": "test-token"}
),
@@ -330,11 +328,11 @@ def test_request_entry_waiting_participant_public_room(settings):
# Verify the lobby cookie was set
cookie = response.cookies.get("mocked-cookie")
assert cookie is not None
assert cookie.value == "2f7f162fe7d1421b90e702bfbfbf8def"
assert cookie.value == "participant1"
# Verify response content matches expected structure and values
assert response.json() == {
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "participant1",
"username": "user1",
"status": "accepted",
"color": "#123456",
@@ -381,7 +379,7 @@ def test_allow_participant_to_enter_anonymous():
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
{"participant_id": "test-id", "allow_entry": True},
)
assert response.status_code == 401
@@ -396,7 +394,7 @@ def test_allow_participant_to_enter_non_owner():
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
{"participant_id": "test-id", "allow_entry": True},
)
assert response.status_code == 403
@@ -414,7 +412,7 @@ def test_allow_participant_to_enter_public_room():
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
{"participant_id": "test-id", "allow_entry": True},
)
assert response.status_code == 404
@@ -437,9 +435,9 @@ def test_allow_participant_to_enter_success(settings, allow_entry, updated_statu
settings.LOBBY_KEY_PREFIX = "mocked-cache-prefix"
cache.set(
f"mocked-cache-prefix_{room.id!s}_2f7f162fe7d1421b90e702bfbfbf8def",
f"mocked-cache-prefix_{room.id!s}_participant1",
{
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "test-id",
"status": "waiting",
"username": "foo",
"color": "123",
@@ -448,18 +446,13 @@ def test_allow_participant_to_enter_success(settings, allow_entry, updated_statu
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{
"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def",
"allow_entry": allow_entry,
},
{"participant_id": "participant1", "allow_entry": allow_entry},
)
assert response.status_code == 200
assert response.json() == {"message": "Participant was updated."}
participant_data = cache.get(
f"mocked-cache-prefix_{room.id!s}_2f7f162fe7d1421b90e702bfbfbf8def"
)
participant_data = cache.get(f"mocked-cache-prefix_{room.id!s}_participant1")
assert participant_data.get("status") == updated_status
@@ -475,14 +468,12 @@ def test_allow_participant_to_enter_participant_not_found(settings):
settings.LOBBY_KEY_PREFIX = "mocked-cache-prefix"
participant_data = cache.get(
f"mocked-cache-prefix_{room.id!s}_2f7f162fe7d1421b90e702bfbfbf8def"
)
participant_data = cache.get(f"mocked-cache-prefix_{room.id!s}_test-id")
assert participant_data is None
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
{"participant_id": "test-id", "allow_entry": True},
)
assert response.status_code == 404
@@ -572,18 +563,18 @@ def test_list_waiting_participants_success(settings):
# Add participants in the lobby
cache.set(
f"mocked-cache-prefix_{room.id}_2f7f162fe7d1421b90e702bfbfbf8def",
f"mocked-cache-prefix_{room.id}_participant1",
{
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "participant1",
"username": "user1",
"status": "waiting",
"color": "#123456",
},
)
cache.set(
f"mocked-cache-prefix_{room.id}_f4ca3ab8a6c04ad88097b8da33f60f10",
f"mocked-cache-prefix_{room.id}_participant2",
{
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
"id": "participant2",
"username": "user2",
"status": "waiting",
"color": "#654321",
@@ -597,13 +588,13 @@ def test_list_waiting_participants_success(settings):
participants = response.json().get("participants")
assert sorted(participants, key=lambda p: p["id"]) == [
{
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"id": "participant1",
"username": "user1",
"status": "waiting",
"color": "#123456",
},
{
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
"id": "participant2",
"username": "user2",
"status": "waiting",
"color": "#654321",
@@ -32,7 +32,6 @@ def test_api_rooms_retrieve_anonymous_private_pk():
"id": str(room.id),
"is_administrable": False,
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -52,7 +51,6 @@ def test_api_rooms_retrieve_anonymous_trusted_pk():
"id": str(room.id),
"is_administrable": False,
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -71,7 +69,6 @@ def test_api_rooms_retrieve_anonymous_private_pk_no_dashes():
"id": str(room.id),
"is_administrable": False,
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -88,7 +85,6 @@ def test_api_rooms_retrieve_anonymous_private_slug():
"id": str(room.id),
"is_administrable": False,
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -105,7 +101,6 @@ def test_api_rooms_retrieve_anonymous_private_slug_not_normalized():
"id": str(room.id),
"is_administrable": False,
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -214,7 +209,6 @@ def test_api_rooms_retrieve_anonymous_public(mock_token):
"token": "foo",
},
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -257,7 +251,6 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
"token": "foo",
},
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -302,7 +295,6 @@ def test_api_rooms_retrieve_authenticated_trusted(mock_token):
"token": "foo",
},
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -332,7 +324,6 @@ def test_api_rooms_retrieve_authenticated():
"id": str(room.id),
"is_administrable": False,
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -381,7 +372,6 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
"token": "foo",
},
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -468,7 +458,6 @@ def test_api_rooms_retrieve_administrators(
"token": "foo",
},
"name": room.name,
"pin_code": room.pin_code,
"slug": room.slug,
}
@@ -2,6 +2,7 @@
Test LiveKit webhook endpoint on the rooms API.
"""
# ruff: noqa: PLR0913
# pylint: disable=R0913,W0621,R0917,W0613
import base64
import hashlib
@@ -24,7 +25,7 @@ def webhook_event_data():
"name": "00000000-0000-0000-0000-000000000000",
"emptyTimeout": 300,
"creationTime": "1692627281",
"turnPassword": "fake-turn-password",
"turnPassword": "2Pvdj+/WV1xV4EkB8klJ9xkXDWY=",
"enabledCodecs": [
{"mime": "audio/opus"},
{"mime": "video/H264"},
@@ -143,7 +144,7 @@ def test_handled_event_type(
def test_unhandled_event_type(client, mock_livekit_config):
"""Should return 200 for event types that have no handler."""
event_data = json.dumps({"event": "participant_joined"})
event_data = json.dumps({"event": "room_started"})
hash64 = base64.b64encode(hashlib.sha256(event_data.encode()).digest()).decode()
token = api.AccessToken(
@@ -1,16 +1,13 @@
"""
Test LiveKitEvents service.
"""
# pylint: disable=W0621,W0613, W0212, E0611
# pylint: disable=W0621,W0613, W0212
import uuid
from unittest import mock
import pytest
from livekit.api import EgressStatus
from core.factories import RecordingFactory, RoomFactory
from core.recording.services.recording_events import RecordingEventsService
from core.services.livekit_events import (
ActionFailedError,
AuthenticationError,
@@ -20,8 +17,6 @@ from core.services.livekit_events import (
api,
)
from core.services.lobby import LobbyService
from core.services.telephony import TelephonyException, TelephonyService
from core.utils import NotificationError
pytestmark = pytest.mark.django_db
@@ -58,251 +53,43 @@ def test_initialization(
mock_token_verifier.assert_called_once_with(api_key, api_secret)
mock_webhook_receiver.assert_called_once_with(mock_token_verifier.return_value)
assert isinstance(service.lobby_service, LobbyService)
assert isinstance(service.telephony_service, TelephonyService)
assert isinstance(service.recording_events, RecordingEventsService)
@pytest.mark.parametrize(
("mode", "notification_type"),
(
("screen_recording", "screenRecordingLimitReached"),
("transcript", "transcriptionLimitReached"),
),
)
@mock.patch("core.utils.notify_participants")
def test_handle_egress_ended_success(mock_notify, mode, notification_type, service):
"""Should successfully stop recording and notifies all participant."""
recording = RecordingFactory(worker_id="worker-1", mode=mode, status="active")
mock_data = mock.MagicMock()
mock_data.egress_info.egress_id = recording.worker_id
mock_data.egress_info.status = EgressStatus.EGRESS_LIMIT_REACHED
service._handle_egress_ended(mock_data)
mock_notify.assert_called_once_with(
room_name=str(recording.room.id), notification_data={"type": notification_type}
)
recording.refresh_from_db()
assert recording.status == "stopped"
@mock.patch("core.utils.notify_participants")
def test_handle_egress_ended_notification_fails(mock_notify, service):
"""Should raise ActionFailedError when notification fails but still stop recording."""
recording = RecordingFactory(worker_id="worker-1", status="active")
mock_data = mock.MagicMock()
mock_data.egress_info.egress_id = recording.worker_id
mock_data.egress_info.status = EgressStatus.EGRESS_LIMIT_REACHED
mock_notify.side_effect = NotificationError("Error notifying")
with pytest.raises(
ActionFailedError,
match=r"Failed to process limit reached event for recording .+",
):
service._handle_egress_ended(mock_data)
recording.refresh_from_db()
assert recording.status == "stopped"
@mock.patch("core.utils.notify_participants")
def test_handle_egress_ended_recording_not_found(mock_notify, service):
"""Should raise ActionFailedError when recording doesn't exist."""
recording = RecordingFactory(worker_id="worker-1", status="active")
mock_data = mock.MagicMock()
mock_data.egress_info.egress_id = "worker-2"
mock_data.egress_info.status = EgressStatus.EGRESS_LIMIT_REACHED
with pytest.raises(
ActionFailedError, match=r"Recording with worker ID .+ does not exist"
):
service._handle_egress_ended(mock_data)
mock_notify.assert_not_called()
recording.refresh_from_db()
assert recording.status == "active"
@mock.patch("core.utils.notify_participants")
def test_handle_egress_ended_recording_not_active(mock_notify, service):
"""Should ignore non-active recordings."""
recording = RecordingFactory(worker_id="worker-1", status="failed_to_stop")
mock_data = mock.MagicMock()
mock_data.egress_info.egress_id = "worker-1"
mock_data.egress_info.status = EgressStatus.EGRESS_LIMIT_REACHED
service._handle_egress_ended(mock_data)
mock_notify.assert_not_called()
recording.refresh_from_db()
assert recording.status == "failed_to_stop"
@mock.patch("core.utils.notify_participants")
def test_handle_egress_ended_recording_not_limit_reached(mock_notify, service):
"""Should ignore egress non-limit-reached statuses."""
recording = RecordingFactory(worker_id="worker-1", status="stopped")
mock_data = mock.MagicMock()
mock_data.egress_info.egress_id = "worker-1"
mock_data.egress_info.status = EgressStatus.EGRESS_COMPLETE
service._handle_egress_ended(mock_data)
mock_notify.assert_not_called()
assert recording.status == "stopped"
@mock.patch.object(LobbyService, "clear_room_cache")
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
def test_handle_room_finished_clears_cache_and_deletes_dispatch_rule(
mock_delete_dispatch_rule, mock_clear_cache, service, settings
):
"""Should clear lobby cache and delete telephony dispatch rule when room finishes."""
settings.ROOM_TELEPHONY_ENABLED = True
def test_handle_room_finished(mock_clear_cache, service):
"""Should clear lobby cache when room is finished."""
mock_room_name = uuid.uuid4()
mock_data = mock.MagicMock()
mock_data.room.name = str(mock_room_name)
service._handle_room_finished(mock_data)
mock_delete_dispatch_rule.assert_called_once_with(mock_room_name)
mock_clear_cache.assert_called_once_with(mock_room_name)
@mock.patch.object(LobbyService, "clear_room_cache")
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
def test_handle_room_finished_skips_telephony_when_disabled(
mock_delete_dispatch_rule, mock_clear_cache, service, settings
):
"""Should clear lobby cache but skip dispatch rule deletion when telephony is disabled."""
settings.ROOM_TELEPHONY_ENABLED = False
mock_room_name = uuid.uuid4()
mock_data = mock.MagicMock()
mock_data.room.name = str(mock_room_name)
service._handle_room_finished(mock_data)
mock_delete_dispatch_rule.assert_not_called()
mock_clear_cache.assert_called_once_with(mock_room_name)
@mock.patch.object(
LobbyService, "clear_room_cache", side_effect=Exception("Test error")
)
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
def test_handle_room_finished_raises_error_when_cache_clearing_fails(
mock_delete_dispatch_rule, mock_clear_cache, service, settings
):
"""Should raise ActionFailedError when lobby cache clearing fails when room finishes."""
settings.ROOM_TELEPHONY_ENABLED = True
def test_handle_room_finished_error(mock_clear_cache, service):
"""Should raise ActionFailedError when processing fails."""
mock_data = mock.MagicMock()
mock_data.room.name = "00000000-0000-0000-0000-000000000000"
expected_error = (
"Failed to clear room cache for room 00000000-0000-0000-0000-000000000000"
)
with pytest.raises(ActionFailedError, match=expected_error):
service._handle_room_finished(mock_data)
mock_delete_dispatch_rule.assert_called_once_with(
uuid.UUID("00000000-0000-0000-0000-000000000000")
)
@mock.patch.object(LobbyService, "clear_room_cache")
@mock.patch.object(
TelephonyService,
"delete_dispatch_rule",
side_effect=TelephonyException("Test error"),
)
def test_handle_room_finished_raises_error_when_telephony_deletion_fails(
mock_delete_dispatch_rule, mock_clear_cache, service, settings
):
"""Should raise ActionFailedError when dispatch rule deletion fails when room finishes."""
settings.ROOM_TELEPHONY_ENABLED = True
mock_data = mock.MagicMock()
mock_data.room.name = "00000000-0000-0000-0000-000000000000"
expected_error = (
"Failed to delete telephony dispatch rule for room "
"00000000-0000-0000-0000-000000000000"
)
with pytest.raises(ActionFailedError, match=expected_error):
service._handle_room_finished(mock_data)
mock_clear_cache.assert_not_called()
def test_handle_room_finished_raises_error_for_invalid_room_name(service):
"""Should raise ActionFailedError when room name format is invalid when room finishes."""
mock_data = mock.MagicMock()
mock_data.room.name = "invalid"
with pytest.raises(
ActionFailedError, match="Failed to process room finished event"
):
service._handle_room_finished(mock_data)
@mock.patch.object(TelephonyService, "create_dispatch_rule")
def test_handle_room_started_creates_dispatch_rule_successfully(
mock_create_dispatch_rule, service, settings
):
"""Should create telephony dispatch rule when room starts successfully."""
settings.ROOM_TELEPHONY_ENABLED = True
room = RoomFactory()
mock_data = mock.MagicMock()
mock_data.room.name = str(room.id)
service._handle_room_started(mock_data)
mock_create_dispatch_rule.assert_called_once_with(room)
@mock.patch.object(TelephonyService, "create_dispatch_rule")
def test_handle_room_started_skips_dispatch_rule_when_telephony_disabled(
mock_create_dispatch_rule, service, settings
):
"""Should skip creating telephony dispatch rule when telephony is disabled during room start."""
settings.ROOM_TELEPHONY_ENABLED = False
room = RoomFactory()
mock_data = mock.MagicMock()
mock_data.room.name = str(room.id)
service._handle_room_started(mock_data)
mock_create_dispatch_rule.assert_not_called()
def test_handle_room_started_raises_error_for_invalid_room_name(service):
"""Should raise ActionFailedError when room name format is invalid when room starts."""
def test_handle_room_finished_invalid_room_name(service):
"""Should raise ActionFailedError when processing fails."""
mock_data = mock.MagicMock()
mock_data.room.name = "invalid"
with pytest.raises(ActionFailedError, match="Failed to process room started event"):
service._handle_room_started(mock_data)
def test_handle_room_started_raises_error_for_nonexistent_room(service):
"""Should raise ActionFailedError when a room starts that doesn't exist in the database."""
mock_data = mock.MagicMock()
mock_data.room.name = str(uuid.uuid4())
expected_error = f"Room with ID {mock_data.room.name} does not exist"
with pytest.raises(ActionFailedError, match=expected_error):
service._handle_room_started(mock_data)
with pytest.raises(
ActionFailedError, match="Failed to process room finished event"
):
service._handle_room_finished(mock_data)
@mock.patch.object(
+117 -7
View File
@@ -5,6 +5,7 @@ Test lobby service.
# pylint: disable=W0621,W0613, W0212, R0913
# ruff: noqa: PLR0913
import json
import uuid
from unittest import mock
@@ -13,17 +14,18 @@ from django.core.cache import cache
from django.http import HttpResponse
import pytest
from livekit.api import TwirpError
from core.factories import RoomFactory
from core.models import RoomAccessLevel
from core.services.lobby import (
LobbyNotificationError,
LobbyParticipant,
LobbyParticipantNotFound,
LobbyParticipantParsingError,
LobbyParticipantStatus,
LobbyService,
)
from core.utils import NotificationError
pytestmark = pytest.mark.django_db
@@ -412,7 +414,7 @@ def test_refresh_waiting_status(mock_cache, lobby_service, participant_id):
# pylint: disable=R0917
@mock.patch("core.services.lobby.cache")
@mock.patch("core.utils.generate_color")
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.lobby.LobbyService.notify_participants")
def test_enter_success(
mock_notify,
mock_generate_color,
@@ -441,15 +443,13 @@ def test_enter_success(
participant.to_dict(),
timeout=settings.LOBBY_WAITING_TIMEOUT,
)
mock_notify.assert_called_once_with(
room_name=str(room.id), notification_data={"type": "participantWaiting"}
)
mock_notify.assert_called_once_with(room_id=room.id)
# pylint: disable=R0917
@mock.patch("core.services.lobby.cache")
@mock.patch("core.utils.generate_color")
@mock.patch("core.utils.notify_participants")
@mock.patch("core.services.lobby.LobbyService.notify_participants")
def test_enter_with_notification_error(
mock_notify,
mock_generate_color,
@@ -460,7 +460,7 @@ def test_enter_with_notification_error(
):
"""Test participant entry with notification error."""
mock_generate_color.return_value = "#123456"
mock_notify.side_effect = NotificationError("Error notifying")
mock_notify.side_effect = LobbyNotificationError("Error notifying")
lobby_service._get_cache_key = mock.Mock(return_value="mocked_cache_key")
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
@@ -776,6 +776,116 @@ def test_update_participant_status_success(mock_cache, lobby_service, participan
lobby_service._get_cache_key.assert_called_once_with(room.id, participant_id)
@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_success_no_room(mock_create_livekit_client, lobby_service):
"""Test the notify_participants method when the LiveKit room doesn't exist yet."""
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock()
# Create a proper response object with an empty rooms list
class MockResponse:
"""LiveKit API response mock with empty rooms list."""
rooms = []
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api_instance
# Act
lobby_service.notify_participants(room.id)
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify the send_data method was not called since no room exists
mock_api_instance.room.send_data.assert_not_called()
# Verify the connection was properly closed
mock_api_instance.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_success(mock_create_livekit_client, lobby_service):
"""Test successful participant notification."""
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock()
class MockResponse:
"""LiveKit API response mock with non-empty rooms list."""
rooms = ["room-1"]
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api_instance
# Call the function
lobby_service.notify_participants(room.id)
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify the send_data method was called
mock_api_instance.room.send_data.assert_called_once()
send_data_request = mock_api_instance.room.send_data.call_args[0][0]
assert send_data_request.room == str(room.id)
assert (
json.loads(send_data_request.data.decode("utf-8"))["type"]
== settings.LOBBY_NOTIFICATION_TYPE
)
assert send_data_request.kind == 0 # RELIABLE mode in Livekit protocol
# Verify aclose was called
mock_api_instance.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_error(mock_create_livekit_client, lobby_service):
"""Test participant notification with API error."""
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock(
side_effect=TwirpError(msg="test error", code=123, status=123)
)
class MockResponse:
"""LiveKit API response mock with non-empty rooms list."""
rooms = ["room-1"]
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api_instance
# Call the function and expect an exception
with pytest.raises(
LobbyNotificationError, match="Failed to notify room participants"
):
lobby_service.notify_participants(room.id)
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify send_data was called
mock_api_instance.room.send_data.assert_called_once()
# Verify aclose was still called after the exception
mock_api_instance.aclose.assert_called_once()
def test_clear_room_cache(settings, lobby_service):
"""Test clearing room cache actually removes entries from cache."""
@@ -11,7 +11,6 @@ from django.core.exceptions import ImproperlyConfigured
import brevo_python
import pytest
import urllib3
from core.services.marketing import (
BrevoMarketingService,
@@ -134,34 +133,6 @@ def test_create_contact_api_error(mock_contact_api):
brevo_service.create_contact(valid_contact_data)
@mock.patch("brevo_python.ContactsApi")
def test_create_contact_timeout_error(mock_contact_api):
"""Test contact creation timeout error handling."""
mock_api = mock_contact_api.return_value
settings.BREVO_API_KEY = "test-api-key"
settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4]
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
valid_contact_data = ContactData(
email="test@example.com",
attributes={"first_name": "Test"},
list_ids=[1, 2],
update_enabled=True,
)
brevo_service = BrevoMarketingService()
mock_api.create_contact.side_effect = urllib3.exceptions.ReadTimeoutError(
pool=mock.Mock(),
url="https://api.brevo.com/v3/endpoint",
message="HTTPSConnectionPool(host='api.brevo.com', port=443): Read timed out.",
)
with pytest.raises(ContactCreationError, match="Failed to create contact in Brevo"):
brevo_service.create_contact(valid_contact_data)
@pytest.fixture
def clear_marketing_cache():
"""Clear marketing service cache between tests."""
@@ -1,305 +0,0 @@
"""
Test telephony service.
"""
# pylint: disable=W0212
from unittest import mock
import pytest
from asgiref.sync import async_to_sync
from livekit.api import TwirpError
from livekit.protocol.sip import (
CreateSIPDispatchRuleRequest,
DeleteSIPDispatchRuleRequest,
ListSIPDispatchRuleRequest,
ListSIPDispatchRuleResponse,
SIPDispatchRule,
SIPDispatchRuleInfo,
)
from core.factories import RoomFactory
from core.models import RoomAccessLevel
from core.services.telephony import TelephonyException, TelephonyService
pytestmark = pytest.mark.django_db
def create_mock_livekit_client():
"""Factory for creating LiveKit client mock."""
mock_api = mock.Mock()
mock_api.sip = mock.Mock()
mock_api.aclose = mock.AsyncMock()
return mock_api
def test_rule_name():
"""Test rule name generation."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
rule_name = telephony_service._rule_name(room.id)
assert rule_name == f"SIP_{str(room.id)}"
@mock.patch("core.utils.create_livekit_client")
def test_create_dispatch_rule_success(mock_client_factory):
"""Test successful dispatch rule creation."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_api = create_mock_livekit_client()
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock()
mock_client_factory.return_value = mock_api
telephony_service.create_dispatch_rule(room)
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
create_request = mock_api.sip.create_sip_dispatch_rule.call_args[1]["create"]
assert isinstance(create_request, CreateSIPDispatchRuleRequest)
assert create_request.name == f"SIP_{str(room.id)}"
assert create_request.rule.dispatch_rule_direct.room_name == str(room.id)
assert create_request.rule.dispatch_rule_direct.pin == str(room.pin_code)
mock_api.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_create_dispatch_rule_api_failure(mock_client_factory):
"""Test dispatch rule creation when API fails."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_api = create_mock_livekit_client()
mock_api.sip.create_sip_dispatch_rule = mock.AsyncMock(
side_effect=TwirpError(msg="Internal server error", code=500, status=500)
)
mock_client_factory.return_value = mock_api
with pytest.raises(TelephonyException, match="Could not create dispatch rule"):
telephony_service.create_dispatch_rule(room)
mock_api.sip.create_sip_dispatch_rule.assert_called_once()
mock_api.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_list_dispatch_rules_ids_success(mock_client_factory):
"""Test successful listing of dispatch rule IDs."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_rules = [
SIPDispatchRuleInfo(
sip_dispatch_rule_id="rule-1",
name=f"SIP_{str(room.id)}",
rule=SIPDispatchRule(),
),
SIPDispatchRuleInfo(
sip_dispatch_rule_id="rule-2", name="OTHER_RULE", rule=SIPDispatchRule()
),
SIPDispatchRuleInfo(
sip_dispatch_rule_id="rule-3",
name=f"SIP_{str(room.id)}",
rule=SIPDispatchRule(),
),
]
mock_api = create_mock_livekit_client()
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
return_value=ListSIPDispatchRuleResponse(items=mock_rules)
)
mock_client_factory.return_value = mock_api
result = async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
assert len(result) == 2
assert "rule-1" in result
assert "rule-3" in result
assert "rule-2" not in result
mock_api.sip.list_sip_dispatch_rule.assert_called_once()
list_request = mock_api.sip.list_sip_dispatch_rule.call_args[1]["list"]
assert isinstance(list_request, ListSIPDispatchRuleRequest)
mock_api.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_list_dispatch_rules_ids_empty_response(mock_client_factory):
"""Test listing dispatch rule IDs when no rules exist."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_api = create_mock_livekit_client()
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
return_value=ListSIPDispatchRuleResponse(items=[])
)
mock_client_factory.return_value = mock_api
result = async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
assert result == []
mock_api.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_list_dispatch_rules_ids_no_matching_rules(mock_client_factory):
"""Test listing dispatch rule IDs when no rules match the room."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_rules = [
SIPDispatchRuleInfo(
sip_dispatch_rule_id="rule-1", name="OTHER_RULE_1", rule=SIPDispatchRule()
),
SIPDispatchRuleInfo(
sip_dispatch_rule_id="rule-2", name="OTHER_RULE_2", rule=SIPDispatchRule()
),
]
mock_api = create_mock_livekit_client()
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
return_value=ListSIPDispatchRuleResponse(items=mock_rules)
)
mock_client_factory.return_value = mock_api
result = async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
assert result == []
mock_api.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_list_dispatch_rules_ids_api_failure(mock_client_factory):
"""Test listing dispatch rule IDs when API fails."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_api = create_mock_livekit_client()
mock_api.sip.list_sip_dispatch_rule = mock.AsyncMock(
side_effect=TwirpError(msg="Internal server error", code=500, status=500)
)
mock_client_factory.return_value = mock_api
with pytest.raises(TelephonyException, match="Could not list dispatch rules"):
async_to_sync(telephony_service._list_dispatch_rules_ids)(room.id)
mock_api.sip.list_sip_dispatch_rule.assert_called_once()
mock_api.aclose.assert_called_once()
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
@mock.patch("core.utils.create_livekit_client")
def test_delete_dispatch_rule_no_rules(mock_client_factory, mock_list_rules):
"""Test deleting dispatch rules when no rules exist."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_list_rules.return_value = []
result = telephony_service.delete_dispatch_rule(room.id)
assert result is False
mock_list_rules.assert_called_once_with(room.id)
mock_client_factory.assert_not_called()
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
@mock.patch("core.utils.create_livekit_client")
def test_delete_dispatch_rule_single_rule(mock_client_factory, mock_list_rules):
"""Test deleting a single dispatch rule."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_list_rules.return_value = ["rule-1"]
mock_api = create_mock_livekit_client()
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock()
mock_client_factory.return_value = mock_api
result = telephony_service.delete_dispatch_rule(room.id)
assert result is True
mock_api.sip.delete_sip_dispatch_rule.assert_called_once()
delete_request = mock_api.sip.delete_sip_dispatch_rule.call_args[1]["delete"]
assert isinstance(delete_request, DeleteSIPDispatchRuleRequest)
assert delete_request.sip_dispatch_rule_id == "rule-1"
mock_api.aclose.assert_called_once()
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
@mock.patch("core.utils.create_livekit_client")
def test_delete_dispatch_rule_multiple_rules(mock_client_factory, mock_list_rules):
"""Test deleting multiple dispatch rules."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_list_rules.return_value = ["rule-1", "rule-2", "rule-3"]
mock_api = create_mock_livekit_client()
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock()
mock_client_factory.return_value = mock_api
result = telephony_service.delete_dispatch_rule(room.id)
assert result is True
assert mock_api.sip.delete_sip_dispatch_rule.call_count == 3
deleted_rule_ids = [
call_args[1]["delete"].sip_dispatch_rule_id
for call_args in mock_api.sip.delete_sip_dispatch_rule.call_args_list
]
assert all(
rule_id in deleted_rule_ids for rule_id in ["rule-1", "rule-2", "rule-3"]
)
mock_api.aclose.assert_called_once()
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
@mock.patch("core.utils.create_livekit_client")
def test_delete_dispatch_rule_partial_failure(mock_client_factory, mock_list_rules):
"""Test deleting multiple dispatch rules when one deletion fails."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_list_rules.return_value = ["rule-1", "rule-2", "rule-3"]
mock_api = create_mock_livekit_client()
call_count = 0
def delete_side_effect(*args, **kwargs):
nonlocal call_count
if call_count == 0:
call_count += 1
return None
raise TwirpError(msg="Deletion failed", code=500, status=500)
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock(
side_effect=delete_side_effect
)
mock_client_factory.return_value = mock_api
with pytest.raises(TelephonyException, match="Could not delete dispatch rules"):
telephony_service.delete_dispatch_rule(room.id)
assert mock_api.sip.delete_sip_dispatch_rule.call_count == 2
mock_api.aclose.assert_called_once()
@mock.patch("core.services.telephony.TelephonyService._list_dispatch_rules_ids")
@mock.patch("core.utils.create_livekit_client")
def test_delete_dispatch_rule_api_failure(mock_client_factory, mock_list_rules):
"""Test deleting dispatch rules when API fails immediately."""
telephony_service = TelephonyService()
room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED, pin_code="1234")
mock_list_rules.return_value = ["rule-1"]
mock_api = create_mock_livekit_client()
mock_api.sip.delete_sip_dispatch_rule = mock.AsyncMock(
side_effect=TwirpError(msg="Internal server error", code=500, status=500)
)
mock_client_factory.return_value = mock_api
with pytest.raises(TelephonyException, match="Could not delete dispatch rules"):
telephony_service.delete_dispatch_rule(room.id)
mock_api.sip.delete_sip_dispatch_rule.assert_called_once()
mock_api.aclose.assert_called_once()
+12 -12
View File
@@ -102,7 +102,7 @@ def test_models_rooms_access_rights_none(django_assert_num_queries):
with django_assert_num_queries(0):
assert room.get_role(None) is None
with django_assert_num_queries(0):
assert room.is_administrator_or_owner(None) is False
assert room.is_administrator(None) is False
with django_assert_num_queries(0):
assert room.is_owner(None) is False
@@ -115,7 +115,7 @@ def test_models_rooms_access_rights_anonymous(django_assert_num_queries):
with django_assert_num_queries(0):
assert room.get_role(user) is None
with django_assert_num_queries(0):
assert room.is_administrator_or_owner(user) is False
assert room.is_administrator(user) is False
with django_assert_num_queries(0):
assert room.is_owner(user) is False
@@ -128,7 +128,7 @@ def test_models_rooms_access_rights_authenticated(django_assert_num_queries):
with django_assert_num_queries(1):
assert room.get_role(user) is None
with django_assert_num_queries(1):
assert room.is_administrator_or_owner(user) is False
assert room.is_administrator(user) is False
with django_assert_num_queries(1):
assert room.is_owner(user) is False
@@ -141,7 +141,7 @@ def test_models_rooms_access_rights_member_direct(django_assert_num_queries):
with django_assert_num_queries(1):
assert room.get_role(user) == "member"
with django_assert_num_queries(1):
assert room.is_administrator_or_owner(user) is False
assert room.is_administrator(user) is False
with django_assert_num_queries(1):
assert room.is_owner(user) is False
@@ -154,7 +154,7 @@ def test_models_rooms_access_rights_administrator_direct(django_assert_num_queri
with django_assert_num_queries(1):
assert room.get_role(user) == "administrator"
with django_assert_num_queries(1):
assert room.is_administrator_or_owner(user) is True
assert room.is_administrator(user) is True
with django_assert_num_queries(1):
assert room.is_owner(user) is False
@@ -167,7 +167,7 @@ def test_models_rooms_access_rights_owner_direct(django_assert_num_queries):
with django_assert_num_queries(1):
assert room.get_role(user) == "owner"
with django_assert_num_queries(1):
assert room.is_administrator_or_owner(user) is True
assert room.is_administrator(user) is True
with django_assert_num_queries(1):
assert room.is_owner(user) is True
@@ -302,25 +302,25 @@ def test_pin_generation_max_retries(mock_randbelow, mock_logger, settings):
RoomFactory(pin_code="12345")
# Assert default max retries is low, 5
# Assert default max retries is low, 3
room1 = RoomFactory()
assert mock_randbelow.call_count == 5
assert mock_randbelow.call_count == 3
assert room1.pin_code is None
mock_logger.assert_called_once_with(
"Failed to generate unique PIN code of length %s after %s attempts", 5, 5
"Failed to generate unique PIN code of length %s after %s attempts", 5, 3
)
mock_logger.reset_mock()
mock_randbelow.reset_mock()
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES = 3
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES = 5
room2 = RoomFactory()
assert mock_randbelow.call_count == 3
assert mock_randbelow.call_count == 5
assert room2.pin_code is None
mock_logger.assert_called_once_with(
"Failed to generate unique PIN code of length %s after %s attempts", 5, 3
"Failed to generate unique PIN code of length %s after %s attempts", 5, 5
)
+1 -107
View File
@@ -2,13 +2,9 @@
Test utils functions
"""
import json
from unittest import mock
import pytest
from livekit.api import TwirpError
from core.utils import NotificationError, create_livekit_client, notify_participants
from core.utils import create_livekit_client
@mock.patch("asyncio.get_running_loop")
@@ -64,105 +60,3 @@ def test_create_livekit_client_custom_configuration(
create_livekit_client(custom_configuration)
mock_livekit_api.assert_called_once_with(**custom_configuration, session=None)
@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_error(mock_create_livekit_client):
"""Test participant notification with API error."""
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock(
side_effect=TwirpError(msg="test error", code=123, status=123)
)
class MockResponse:
"""LiveKit API response mock with non-empty rooms list."""
rooms = ["room-1"]
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api_instance
# Call the function and expect an exception
with pytest.raises(NotificationError, match="Failed to notify room participants"):
notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify send_data was called
mock_api_instance.room.send_data.assert_called_once()
# Verify aclose was still called after the exception
mock_api_instance.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_success_no_room(mock_create_livekit_client):
"""Test the notify_participants function when the LiveKit room doesn't exist."""
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock()
# Create a proper response object with an empty rooms list
class MockResponse:
"""LiveKit API response mock with empty rooms list."""
rooms = []
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api_instance
notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify the send_data method was not called since no room exists
mock_api_instance.room.send_data.assert_not_called()
# Verify the connection was properly closed
mock_api_instance.aclose.assert_called_once()
@mock.patch("core.utils.create_livekit_client")
def test_notify_participants_success(mock_create_livekit_client):
"""Test successful participant notification."""
# Set up the mock LiveKitAPI and its behavior
mock_api_instance = mock.Mock()
mock_api_instance.room = mock.Mock()
mock_api_instance.room.send_data = mock.AsyncMock()
class MockResponse:
"""LiveKit API response mock with non-empty rooms list."""
rooms = ["room-1"]
mock_api_instance.room.list_rooms = mock.AsyncMock(return_value=MockResponse())
mock_api_instance.aclose = mock.AsyncMock()
mock_create_livekit_client.return_value = mock_api_instance
# Call the function
notify_participants(room_name="room-number-1", notification_data={"foo": "foo"})
# Verify that the service checked for existing rooms
mock_api_instance.room.list_rooms.assert_called_once()
# Verify the send_data method was called
mock_api_instance.room.send_data.assert_called_once()
send_data_request = mock_api_instance.room.send_data.call_args[0][0]
assert send_data_request.room == "room-number-1"
assert json.loads(send_data_request.data.decode("utf-8")) == {"foo": "foo"}
assert send_data_request.kind == 0 # RELIABLE mode in Livekit protocol
# Verify aclose was called
mock_api_instance.aclose.assert_called_once()
+1 -43
View File
@@ -15,15 +15,7 @@ from django.core.files.storage import default_storage
import aiohttp
import botocore
from asgiref.sync import async_to_sync
from livekit.api import ( # pylint: disable=E0611
AccessToken,
ListRoomsRequest,
LiveKitAPI,
SendDataRequest,
TwirpError,
VideoGrants,
)
from livekit.api import AccessToken, LiveKitAPI, VideoGrants
def generate_color(identity: str) -> str:
@@ -166,37 +158,3 @@ def create_livekit_client(custom_configuration=None):
configuration = custom_configuration or settings.LIVEKIT_CONFIGURATION
return LiveKitAPI(session=custom_session, **configuration)
class NotificationError(Exception):
"""Notification delivery to room participants fails."""
@async_to_sync
async def notify_participants(room_name: str, notification_data: dict):
"""Send notification data to all participants in a LiveKit room."""
lkapi = create_livekit_client()
try:
room_response = await lkapi.room.list_rooms(
ListRoomsRequest(
names=[room_name],
)
)
# Check if the room exists
if not room_response.rooms:
return
await lkapi.room.send_data(
SendDataRequest(
room=room_name,
data=json.dumps(notification_data).encode("utf-8"),
kind="RELIABLE",
)
)
except TwirpError as e:
raise NotificationError("Failed to notify room participants") from e
finally:
await lkapi.aclose()
Binary file not shown.
+21 -38
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,15 +29,7 @@ msgstr "Berechtigungen"
msgid "Important dates"
msgstr "Wichtige Daten"
#: core/admin.py:147
msgid "No owner"
msgstr "Kein Eigentümer"
#: core/admin.py:150
msgid "Multiple owners"
msgstr "Mehrere Eigentümer"
#: core/api/serializers.py:67
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
@@ -251,8 +243,7 @@ msgstr "PIN-Code für den Raum"
#: core/models.py:392
msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr ""
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
msgstr "Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
#: core/models.py:398 core/models.py:552
msgid "Room"
@@ -367,9 +358,9 @@ msgid "Make sure you have a stable internet connection"
msgstr "Stellen Sie sicher, dass Sie eine stabile Internetverbindung haben"
#: core/templates/mail/html/invitation.html:248
#: core/templates/mail/html/screen_recording.html:245
#: core/templates/mail/html/screen_recording.html:240
#: core/templates/mail/text/invitation.txt:21
#: core/templates/mail/text/screen_recording.txt:23
#: core/templates/mail/text/screen_recording.txt:22
#, python-format
msgid " Thank you for using %(brandname)s. "
msgstr " Vielen Dank für die Nutzung von %(brandname)s. "
@@ -395,41 +386,33 @@ msgstr ""
msgid " The recording will expire in %(days)s days. "
msgstr " Die Aufzeichnung wird in %(days)s Tagen ablaufen. "
#: core/templates/mail/html/screen_recording.html:200
#: core/templates/mail/text/screen_recording.txt:9
msgid ""
" Sharing the recording via link is not yet available. Only organizers can "
"download it. "
msgstr ""
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisatoren können sie herunterladen. "
#: core/templates/mail/html/screen_recording.html:206
#: core/templates/mail/text/screen_recording.txt:11
#: core/templates/mail/html/screen_recording.html:201
#: core/templates/mail/text/screen_recording.txt:10
msgid "To keep this recording permanently:"
msgstr "So speichern Sie diese Aufzeichnung dauerhaft:"
#: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13
#: core/templates/mail/html/screen_recording.html:203
#: core/templates/mail/text/screen_recording.txt:12
msgid "Click the \"Open\" button below "
msgstr "Klicken Sie auf den Button „Öffnen“ unten "
#: core/templates/mail/html/screen_recording.html:209
#: core/templates/mail/text/screen_recording.txt:14
#: core/templates/mail/html/screen_recording.html:204
#: core/templates/mail/text/screen_recording.txt:13
msgid "Use the \"Download\" button in the interface "
msgstr "Verwenden Sie den Button „Herunterladen“ in der Oberfläche "
#: core/templates/mail/html/screen_recording.html:210
#: core/templates/mail/text/screen_recording.txt:15
#: core/templates/mail/html/screen_recording.html:205
#: core/templates/mail/text/screen_recording.txt:14
msgid "Save the file to your preferred location"
msgstr "Speichern Sie die Datei an einem gewünschten Ort"
#: core/templates/mail/html/screen_recording.html:221
#: core/templates/mail/text/screen_recording.txt:17
#: core/templates/mail/html/screen_recording.html:216
#: core/templates/mail/text/screen_recording.txt:16
msgid "Open"
msgstr "Öffnen"
#: core/templates/mail/html/screen_recording.html:230
#: core/templates/mail/text/screen_recording.txt:19
#: core/templates/mail/html/screen_recording.html:225
#: core/templates/mail/text/screen_recording.txt:18
#, python-format
msgid ""
" If you have any questions or need assistance, please contact our support "
@@ -438,18 +421,18 @@ msgstr ""
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
"an unser Support-Team unter %(support_email)s. "
#: meet/settings.py:163
#: meet/settings.py:162
msgid "English"
msgstr "Englisch"
#: meet/settings.py:164
#: meet/settings.py:163
msgid "French"
msgstr "Französisch"
#: meet/settings.py:165
#: meet/settings.py:164
msgid "Dutch"
msgstr "Niederländisch"
#: meet/settings.py:166
#: meet/settings.py:165
msgid "German"
msgstr "Deutsch"
Binary file not shown.
+20 -37
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,15 +29,7 @@ msgstr "Permissions"
msgid "Important dates"
msgstr "Important dates"
#: core/admin.py:147
msgid "No owner"
msgstr "No owner"
#: core/admin.py:150
msgid "Multiple owners"
msgstr "Multiple owners"
#: core/api/serializers.py:67
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr "You must be administrator or owner of a room to add accesses to it."
@@ -361,9 +353,9 @@ msgid "Make sure you have a stable internet connection"
msgstr "Make sure you have a stable internet connection"
#: core/templates/mail/html/invitation.html:248
#: core/templates/mail/html/screen_recording.html:245
#: core/templates/mail/html/screen_recording.html:240
#: core/templates/mail/text/invitation.txt:21
#: core/templates/mail/text/screen_recording.txt:23
#: core/templates/mail/text/screen_recording.txt:22
#, python-format
msgid " Thank you for using %(brandname)s. "
msgstr " Thank you for using %(brandname)s. "
@@ -389,42 +381,33 @@ msgstr ""
msgid " The recording will expire in %(days)s days. "
msgstr " The recording will expire in %(days)s days. "
#: core/templates/mail/html/screen_recording.html:200
#: core/templates/mail/text/screen_recording.txt:9
msgid ""
" Sharing the recording via link is not yet available. Only organizers can "
"download it. "
msgstr ""
" Sharing the recording via link is not yet available. Only organizers can "
"download it. "
#: core/templates/mail/html/screen_recording.html:206
#: core/templates/mail/text/screen_recording.txt:11
#: core/templates/mail/html/screen_recording.html:201
#: core/templates/mail/text/screen_recording.txt:10
msgid "To keep this recording permanently:"
msgstr "To keep this recording permanently:"
#: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13
#: core/templates/mail/html/screen_recording.html:203
#: core/templates/mail/text/screen_recording.txt:12
msgid "Click the \"Open\" button below "
msgstr "Click the \"Open\" button below "
#: core/templates/mail/html/screen_recording.html:209
#: core/templates/mail/text/screen_recording.txt:14
#: core/templates/mail/html/screen_recording.html:204
#: core/templates/mail/text/screen_recording.txt:13
msgid "Use the \"Download\" button in the interface "
msgstr "Use the \"Download\" button in the interface "
#: core/templates/mail/html/screen_recording.html:210
#: core/templates/mail/text/screen_recording.txt:15
#: core/templates/mail/html/screen_recording.html:205
#: core/templates/mail/text/screen_recording.txt:14
msgid "Save the file to your preferred location"
msgstr "Save the file to your preferred location"
#: core/templates/mail/html/screen_recording.html:221
#: core/templates/mail/text/screen_recording.txt:17
#: core/templates/mail/html/screen_recording.html:216
#: core/templates/mail/text/screen_recording.txt:16
msgid "Open"
msgstr "Open"
#: core/templates/mail/html/screen_recording.html:230
#: core/templates/mail/text/screen_recording.txt:19
#: core/templates/mail/html/screen_recording.html:225
#: core/templates/mail/text/screen_recording.txt:18
#, python-format
msgid ""
" If you have any questions or need assistance, please contact our support "
@@ -433,18 +416,18 @@ msgstr ""
" If you have any questions or need assistance, please contact our support "
"team at %(support_email)s. "
#: meet/settings.py:163
#: meet/settings.py:162
msgid "English"
msgstr "English"
#: meet/settings.py:164
#: meet/settings.py:163
msgid "French"
msgstr "French"
#: meet/settings.py:165
#: meet/settings.py:164
msgid "Dutch"
msgstr "Dutch"
#: meet/settings.py:166
#: meet/settings.py:165
msgid "German"
msgstr "German"
Binary file not shown.
+26 -42
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,15 +29,7 @@ msgstr "Permissions"
msgid "Important dates"
msgstr "Dates importantes"
#: core/admin.py:147
msgid "No owner"
msgstr "Pas de propriétaire"
#: core/admin.py:150
msgid "Multiple owners"
msgstr "Plusieurs propriétaires"
#: core/api/serializers.py:67
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
@@ -138,8 +130,8 @@ msgid ""
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
"_ characters."
msgstr ""
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
"chiffres et les caractères @/./+/-/_."
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, "
"des chiffres et les caractères @/./+/-/_."
#: core/models.py:144
msgid "sub"
@@ -252,8 +244,7 @@ msgstr "Code PIN de la salle"
#: core/models.py:392
msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr ""
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
msgstr "Code unique à n chiffres qui identifie cette salle en mode téléphonique."
#: core/models.py:398 core/models.py:552
msgid "Room"
@@ -272,8 +263,9 @@ msgid ""
"Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking."
msgstr ""
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant "
"est conservé même lorsque le Worker s'arrête, permettant un suivi "
"facile."
#: core/models.py:573
msgid "Recording mode"
@@ -367,9 +359,9 @@ msgid "Make sure you have a stable internet connection"
msgstr "Assurez-vous d'avoir une connexion Internet stable"
#: core/templates/mail/html/invitation.html:248
#: core/templates/mail/html/screen_recording.html:245
#: core/templates/mail/html/screen_recording.html:240
#: core/templates/mail/text/invitation.txt:21
#: core/templates/mail/text/screen_recording.txt:23
#: core/templates/mail/text/screen_recording.txt:22
#, python-format
msgid " Thank you for using %(brandname)s. "
msgstr " Merci d'utiliser %(brandname)s. "
@@ -395,41 +387,33 @@ msgstr ""
msgid " The recording will expire in %(days)s days. "
msgstr " L'enregistrement expirera dans %(days)s jours. "
#: core/templates/mail/html/screen_recording.html:200
#: core/templates/mail/text/screen_recording.txt:9
msgid ""
" Sharing the recording via link is not yet available. Only organizers can "
"download it. "
msgstr ""
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls les organisateurs peuvent le télécharger."
#: core/templates/mail/html/screen_recording.html:206
#: core/templates/mail/text/screen_recording.txt:11
#: core/templates/mail/html/screen_recording.html:201
#: core/templates/mail/text/screen_recording.txt:10
msgid "To keep this recording permanently:"
msgstr "Pour conserver cet enregistrement de façon permanente :"
#: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13
#: core/templates/mail/html/screen_recording.html:203
#: core/templates/mail/text/screen_recording.txt:12
msgid "Click the \"Open\" button below "
msgstr "Cliquez sur le bouton \"Ouvrir\" ci-dessous "
#: core/templates/mail/html/screen_recording.html:209
#: core/templates/mail/text/screen_recording.txt:14
#: core/templates/mail/html/screen_recording.html:204
#: core/templates/mail/text/screen_recording.txt:13
msgid "Use the \"Download\" button in the interface "
msgstr "Utilisez le bouton \"Télécharger\" dans l'interface "
#: core/templates/mail/html/screen_recording.html:210
#: core/templates/mail/text/screen_recording.txt:15
#: core/templates/mail/html/screen_recording.html:205
#: core/templates/mail/text/screen_recording.txt:14
msgid "Save the file to your preferred location"
msgstr "Enregistrez le fichier à l'emplacement de votre choix"
#: core/templates/mail/html/screen_recording.html:221
#: core/templates/mail/text/screen_recording.txt:17
#: core/templates/mail/html/screen_recording.html:216
#: core/templates/mail/text/screen_recording.txt:16
msgid "Open"
msgstr "Ouvrir"
#: core/templates/mail/html/screen_recording.html:230
#: core/templates/mail/text/screen_recording.txt:19
#: core/templates/mail/html/screen_recording.html:225
#: core/templates/mail/text/screen_recording.txt:18
#, python-format
msgid ""
" If you have any questions or need assistance, please contact our support "
@@ -438,18 +422,18 @@ msgstr ""
" Si vous avez des questions ou besoin d'assistance, veuillez contacter notre "
"équipe d'assistance à %(support_email)s. "
#: meet/settings.py:163
#: meet/settings.py:162
msgid "English"
msgstr "Anglais"
#: meet/settings.py:164
#: meet/settings.py:163
msgid "French"
msgstr "Français"
#: meet/settings.py:165
#: meet/settings.py:164
msgid "Dutch"
msgstr "Néerlandais"
#: meet/settings.py:166
#: meet/settings.py:165
msgid "German"
msgstr "Allemand"
Binary file not shown.
+21 -38
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,15 +29,7 @@ msgstr "Rechten"
msgid "Important dates"
msgstr "Belangrijke datums"
#: core/admin.py:147
msgid "No owner"
msgstr "Geen eigenaar"
#: core/admin.py:150
msgid "Multiple owners"
msgstr "Meerdere eigenaren"
#: core/api/serializers.py:67
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
@@ -247,8 +239,7 @@ msgstr "Pincode van de kamer"
#: core/models.py:392
msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr ""
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
msgstr "Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
#: core/models.py:398 core/models.py:552
msgid "Room"
@@ -362,9 +353,9 @@ msgid "Make sure you have a stable internet connection"
msgstr "Zorg ervoor dat je een stabiele internetverbinding hebt"
#: core/templates/mail/html/invitation.html:248
#: core/templates/mail/html/screen_recording.html:245
#: core/templates/mail/html/screen_recording.html:240
#: core/templates/mail/text/invitation.txt:21
#: core/templates/mail/text/screen_recording.txt:23
#: core/templates/mail/text/screen_recording.txt:22
#, python-format
msgid " Thank you for using %(brandname)s. "
msgstr " Bedankt voor het gebruik van %(brandname)s. "
@@ -390,41 +381,33 @@ msgstr ""
msgid " The recording will expire in %(days)s days. "
msgstr " De opname verloopt over %(days)s dagen. "
#: core/templates/mail/html/screen_recording.html:200
#: core/templates/mail/text/screen_recording.txt:9
msgid ""
" Sharing the recording via link is not yet available. Only organizers can "
"download it. "
msgstr ""
"Het delen van de opname via een link is nog niet beschikbaar. Alleen organisatoren kunnen deze downloaden."
#: core/templates/mail/html/screen_recording.html:206
#: core/templates/mail/text/screen_recording.txt:11
#: core/templates/mail/html/screen_recording.html:201
#: core/templates/mail/text/screen_recording.txt:10
msgid "To keep this recording permanently:"
msgstr "Om deze opname permanent te bewaren:"
#: core/templates/mail/html/screen_recording.html:208
#: core/templates/mail/text/screen_recording.txt:13
#: core/templates/mail/html/screen_recording.html:203
#: core/templates/mail/text/screen_recording.txt:12
msgid "Click the \"Open\" button below "
msgstr "Klik op de \"Openen\"-knop hieronder "
#: core/templates/mail/html/screen_recording.html:209
#: core/templates/mail/text/screen_recording.txt:14
#: core/templates/mail/html/screen_recording.html:204
#: core/templates/mail/text/screen_recording.txt:13
msgid "Use the \"Download\" button in the interface "
msgstr "Gebruik de \"Download\"-knop in de interface "
#: core/templates/mail/html/screen_recording.html:210
#: core/templates/mail/text/screen_recording.txt:15
#: core/templates/mail/html/screen_recording.html:205
#: core/templates/mail/text/screen_recording.txt:14
msgid "Save the file to your preferred location"
msgstr "Sla het bestand op naar je gewenste locatie"
#: core/templates/mail/html/screen_recording.html:221
#: core/templates/mail/text/screen_recording.txt:17
#: core/templates/mail/html/screen_recording.html:216
#: core/templates/mail/text/screen_recording.txt:16
msgid "Open"
msgstr "Openen"
#: core/templates/mail/html/screen_recording.html:230
#: core/templates/mail/text/screen_recording.txt:19
#: core/templates/mail/html/screen_recording.html:225
#: core/templates/mail/text/screen_recording.txt:18
#, python-format
msgid ""
" If you have any questions or need assistance, please contact our support "
@@ -433,18 +416,18 @@ msgstr ""
" Als je vragen hebt of hulp nodig hebt, neem dan contact op met ons support "
"team via %(support_email)s. "
#: meet/settings.py:163
#: meet/settings.py:162
msgid "English"
msgstr "Engels"
#: meet/settings.py:164
#: meet/settings.py:163
msgid "French"
msgstr "Frans"
#: meet/settings.py:165
#: meet/settings.py:164
msgid "Dutch"
msgstr "Nederlands"
#: meet/settings.py:166
#: meet/settings.py:165
msgid "German"
msgstr "Duits"
+3 -3
View File
@@ -3,12 +3,12 @@
meet's sandbox management script.
"""
import os
import sys
from os import environ
if __name__ == "__main__":
environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
environ.setdefault("DJANGO_CONFIGURATION", "Development")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
from configurations.management import execute_from_command_line
+3 -3
View File
@@ -1,13 +1,13 @@
"""Meet celery configuration file."""
from os import environ
import os
from celery import Celery
from configurations.importer import install
# Set the default Django settings module for the 'celery' program.
environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
environ.setdefault("DJANGO_CONFIGURATION", "Development")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
install(check_options=True)
+26 -85
View File
@@ -11,19 +11,18 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
from os import path
import os
from socket import gethostbyname, gethostname
from django.utils.translation import gettext_lazy as _
import sentry_sdk
from configurations import Configuration, values
from lasuite.configuration.values import SecretFileValue
from sentry_sdk.integrations.django import DjangoIntegration
from sentry_sdk.integrations.logging import ignore_logger
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = path.dirname(path.dirname(path.abspath(__file__)))
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
def get_release():
@@ -39,7 +38,7 @@ def get_release():
# Try to get the current release from the version.json file generated by the
# CI during the Docker image build
try:
with open(path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
with open(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
return json.load(version)["version"]
except FileNotFoundError:
return "NA" # Default: not available
@@ -70,11 +69,11 @@ class Base(Configuration):
API_VERSION = "v1.0"
DATA_DIR = values.Value(path.join("/", "data"), environ_name="DATA_DIR")
DATA_DIR = values.Value(os.path.join("/", "data"), environ_name="DATA_DIR")
# Security
ALLOWED_HOSTS = values.ListValue([])
SECRET_KEY = SecretFileValue(None)
SECRET_KEY = values.Value(None)
SILENCED_SYSTEM_CHECKS = values.ListValue([])
ALLOW_UNSECURE_USER_LISTING = values.BooleanValue(
False, environ_name="ALLOW_UNSECURE_USER_LISTING", environ_prefix=None
@@ -94,7 +93,7 @@ class Base(Configuration):
),
"NAME": values.Value("meet", environ_name="DB_NAME", environ_prefix=None),
"USER": values.Value("dinum", environ_name="DB_USER", environ_prefix=None),
"PASSWORD": SecretFileValue(
"PASSWORD": values.Value(
"pass", environ_name="DB_PASSWORD", environ_prefix=None
),
"HOST": values.Value(
@@ -107,9 +106,9 @@ class Base(Configuration):
# Static files (CSS, JavaScript, Images)
STATIC_URL = "/static/"
STATIC_ROOT = path.join(DATA_DIR, "static")
STATIC_ROOT = os.path.join(DATA_DIR, "static")
MEDIA_URL = "/media/"
MEDIA_ROOT = path.join(DATA_DIR, "media")
MEDIA_ROOT = os.path.join(DATA_DIR, "media")
SITE_ID = 1
@@ -129,10 +128,10 @@ class Base(Configuration):
AWS_S3_ENDPOINT_URL = values.Value(
environ_name="AWS_S3_ENDPOINT_URL", environ_prefix=None
)
AWS_S3_ACCESS_KEY_ID = SecretFileValue(
AWS_S3_ACCESS_KEY_ID = values.Value(
environ_name="AWS_S3_ACCESS_KEY_ID", environ_prefix=None
)
AWS_S3_SECRET_ACCESS_KEY = SecretFileValue(
AWS_S3_SECRET_ACCESS_KEY = values.Value(
environ_name="AWS_S3_SECRET_ACCESS_KEY", environ_prefix=None
)
AWS_S3_REGION_NAME = values.Value(
@@ -167,7 +166,7 @@ class Base(Configuration):
)
)
LOCALE_PATHS = (path.join(BASE_DIR, "locale"),)
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
TIME_ZONE = "UTC"
USE_I18N = True
@@ -177,7 +176,7 @@ class Base(Configuration):
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [path.join(BASE_DIR, "templates")],
"DIRS": [os.path.join(BASE_DIR, "templates")],
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
@@ -305,11 +304,6 @@ class Base(Configuration):
# Frontend
FRONTEND_CONFIGURATION = {
# If set, a <link> tag with this URL as href is added to the <head> of the frontend app.
# This is useful if you want to change CSS variables to customize the look of the app.
"custom_css_url": values.Value(
None, environ_name="FRONTEND_CUSTOM_CSS_URL", environ_prefix=None
),
"analytics": values.DictValue(
{}, environ_name="FRONTEND_ANALYTICS", environ_prefix=None
),
@@ -320,30 +314,18 @@ class Base(Configuration):
False, environ_name="FRONTEND_SILENCE_LIVEKIT_DEBUG", environ_prefix=None
),
"is_silent_login_enabled": values.BooleanValue(
True, environ_name="FRONTEND_IS_SILENT_LOGIN_ENABLED", environ_prefix=None
True, environ_name="FRONTEND_IS_SILENT_LOGING_ENABLED", environ_prefix=None
),
"feedback": values.DictValue(
{}, environ_name="FRONTEND_FEEDBACK", environ_prefix=None
),
"use_french_gov_footer": values.BooleanValue(
False, environ_name="FRONTEND_USE_FRENCH_GOV_FOOTER", environ_prefix=None
),
"use_proconnect_button": values.BooleanValue(
False, environ_name="FRONTEND_USE_PROCONNECT_BUTTON", environ_prefix=None
),
"transcript": values.DictValue(
{}, environ_name="FRONTEND_TRANSCRIPT", environ_prefix=None
),
"manifest_link": values.Value(
None, environ_name="FRONTEND_MANIFEST_LINK", environ_prefix=None
),
}
# Mail
EMAIL_BACKEND = values.Value("django.core.mail.backends.smtp.EmailBackend")
EMAIL_HOST = values.Value(None)
EMAIL_HOST_USER = values.Value(None)
EMAIL_HOST_PASSWORD = SecretFileValue(None)
EMAIL_HOST_PASSWORD = values.Value(None)
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
@@ -358,7 +340,7 @@ class Base(Configuration):
# CORS
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True)
CORS_ALLOWED_ORIGINS = values.ListValue([])
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
@@ -378,9 +360,7 @@ class Base(Configuration):
# Session
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
)
SESSION_COOKIE_AGE = 60 * 60 * 12
# OIDC - Authorization Code Flow
OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
@@ -401,7 +381,7 @@ class Base(Configuration):
OIDC_RP_CLIENT_ID = values.Value(
"meet", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
)
OIDC_RP_CLIENT_SECRET = SecretFileValue(
OIDC_RP_CLIENT_SECRET = values.Value(
None,
environ_name="OIDC_RP_CLIENT_SECRET",
environ_prefix=None,
@@ -430,17 +410,6 @@ class Base(Configuration):
OIDC_RP_SCOPES = values.Value(
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
)
OIDC_USE_PKCE = values.BooleanValue(
default=False, environ_name="OIDC_USE_PKCE", environ_prefix=None
)
OIDC_PKCE_CODE_CHALLENGE_METHOD = values.Value(
default="S256",
environ_name="OIDC_PKCE_CODE_CHALLENGE_METHOD",
environ_prefix=None,
)
OIDC_PKCE_CODE_VERIFIER_SIZE = values.IntegerValue(
default=64, environ_name="OIDC_PKCE_CODE_VERIFIER_SIZE", environ_prefix=None
)
LOGIN_REDIRECT_URL = values.Value(
None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
)
@@ -486,20 +455,12 @@ class Base(Configuration):
# Video conference configuration
LIVEKIT_CONFIGURATION = {
"api_key": SecretFileValue(environ_name="LIVEKIT_API_KEY", environ_prefix=None),
"api_secret": SecretFileValue(
"api_key": values.Value(environ_name="LIVEKIT_API_KEY", environ_prefix=None),
"api_secret": values.Value(
environ_name="LIVEKIT_API_SECRET", environ_prefix=None
),
"url": values.Value(environ_name="LIVEKIT_API_URL", environ_prefix=None),
}
LIVEKIT_FORCE_WSS_PROTOCOL = values.BooleanValue(
False, environ_name="LIVEKIT_FORCE_WSS_PROTOCOL", environ_prefix=None
)
LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND = values.BooleanValue(
environ_name="LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND",
environ_prefix=None,
default=False,
)
LIVEKIT_VERIFY_SSL = values.BooleanValue(
True, environ_name="LIVEKIT_VERIFY_SSL", environ_prefix=None
)
@@ -536,7 +497,7 @@ class Base(Configuration):
RECORDING_STORAGE_EVENT_ENABLE = values.BooleanValue(
False, environ_name="RECORDING_STORAGE_EVENT_ENABLE", environ_prefix=None
)
RECORDING_STORAGE_EVENT_TOKEN = SecretFileValue(
RECORDING_STORAGE_EVENT_TOKEN = values.Value(
None, environ_name="RECORDING_STORAGE_EVENT_TOKEN", environ_prefix=None
)
# Number of days before recordings expire - must be synced with bucket lifecycle policy
@@ -544,15 +505,10 @@ class Base(Configuration):
RECORDING_EXPIRATION_DAYS = values.IntegerValue(
None, environ_name="RECORDING_EXPIRATION_DAYS", environ_prefix=None
)
# Recording max duration in milliseconds - must be synced with LiveKit Egress configuration
# Set to None for no max duration
RECORDING_MAX_DURATION = values.IntegerValue(
None, environ_name="RECORDING_MAX_DURATION", environ_prefix=None
)
SUMMARY_SERVICE_ENDPOINT = values.Value(
None, environ_name="SUMMARY_SERVICE_ENDPOINT", environ_prefix=None
)
SUMMARY_SERVICE_API_TOKEN = SecretFileValue(
SUMMARY_SERVICE_API_TOKEN = values.Value(
None, environ_name="SUMMARY_SERVICE_API_TOKEN", environ_prefix=None
)
SCREEN_RECORDING_BASE_URL = values.Value(
@@ -570,7 +526,7 @@ class Base(Configuration):
environ_name="MARKETING_SERVICE_CLASS",
environ_prefix=None,
)
BREVO_API_KEY = SecretFileValue(
BREVO_API_KEY = values.Value(
None, environ_name="BREVO_API_KEY", environ_prefix=None
)
BREVO_API_CONTACT_LIST_IDS = values.ListValue(
@@ -633,16 +589,6 @@ class Base(Configuration):
environ_name="ROOM_TELEPHONY_PIN_MAX_RETRIES",
environ_prefix=None,
)
ROOM_TELEPHONY_PHONE_NUMBER = values.Value(
None,
environ_name="ROOM_TELEPHONY_PHONE_NUMBER",
environ_prefix=None,
)
ROOM_TELEPHONY_DEFAULT_COUNTRY = values.Value(
"US",
environ_name="ROOM_TELEPHONY_DEFAULT_COUNTRY",
environ_prefix=None,
)
# pylint: disable=invalid-name
@property
@@ -690,7 +636,10 @@ class Base(Configuration):
release=get_release(),
integrations=[DjangoIntegration()],
)
sentry_sdk.set_tag("application", "backend")
# Add the application name to the Sentry scope
scope = sentry_sdk.get_global_scope()
scope.set_tag("application", "backend")
# Ignore the logs added by the DockerflowMiddleware
ignore_logger("request.summary")
@@ -854,14 +803,6 @@ class Staging(Production):
"""
class Sandbox(Production):
"""
Sandbox environment settings
nota bene: it should inherit from the Production environment.
"""
class PreProduction(Production):
"""
Pre-production environment settings
+3 -3
View File
@@ -7,11 +7,11 @@ For more information on this file, see
https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/
"""
from os import environ
import os
from configurations.wsgi import get_wsgi_application
environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
environ.setdefault("DJANGO_CONFIGURATION", "Development")
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
application = get_wsgi_application()
+28 -27
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.33"
version = "0.1.22"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,38 +25,39 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.38.42",
"boto3==1.37.24",
"Brotli==1.1.0",
"brevo-python==1.1.2",
"celery[redis]==5.5.3",
"celery[redis]==5.4.0",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-lasuite[all]==0.0.10",
"django-lasuite==0.0.7",
"django-parler==2.3",
"redis==5.2.1",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-redis==5.4.0",
"django-storages[s3]==1.14.5",
"django-timezone-field>=5.1",
"django==5.2.3",
"djangorestframework==3.16.0",
"django==5.1.9",
"djangorestframework==3.15.2",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10",
"factory_boy==3.3.3",
"gunicorn==23.0.0",
"jsonschema==4.24.0",
"markdown==3.8.2",
"jsonschema==4.23.0",
"june-analytics-python==2.3.0",
"markdown==3.7",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.9",
"psycopg[binary]==3.2.6",
"PyJWT==2.10.1",
"python-frontmatter==1.1.0",
"requests==2.32.4",
"sentry-sdk==2.30.0",
"requests==2.32.3",
"sentry-sdk==2.24.1",
"whitenoise==6.9.0",
"mozilla-django-oidc==4.0.1",
"livekit-api==1.0.3",
"aiohttp==3.12.13",
"livekit-api==1.0.2",
"aiohttp==3.11.14",
]
[project.urls]
@@ -67,22 +68,22 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==4.1",
"drf-spectacular-sidecar==2025.6.1",
"freezegun==1.5.2",
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2025.3.1",
"freezegun==1.5.1",
"ipdb==0.13.13",
"ipython==9.3.0",
"pyfakefs==5.9.1",
"ipython==9.0.2",
"pyfakefs==5.8.0",
"pylint-django==2.6.1",
"pylint==3.3.7",
"pytest-cov==6.2.1",
"pytest-django==4.11.1",
"pytest==8.4.1",
"pylint==3.3.6",
"pytest-cov==6.0.0",
"pytest-django==4.10.0",
"pytest==8.3.5",
"pytest-icdiff==0.9",
"pytest-xdist==3.7.0",
"pytest-xdist==3.6.1",
"responses==0.25.7",
"ruff==0.12.0",
"types-requests==2.32.4.20250611",
"ruff==0.11.2",
"types-requests==2.32.0.20250306",
]
[tool.setuptools]
-1
View File
@@ -1,2 +1 @@
VITE_API_BASE_URL=http://localhost:8071/
VITE_APP_TITLE=LaSuite Meet
-1
View File
@@ -1 +0,0 @@
VITE_APP_TITLE=LaSuite Meet
+1 -1
View File
@@ -35,7 +35,7 @@ ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
USER root
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2
-1
View File
@@ -1,7 +1,6 @@
server {
listen 8080;
server_name localhost;
server_tokens off;
root /usr/share/nginx/html;
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/assets/icon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>%VITE_APP_TITLE%</title>
<title>Visio</title>
</head>
<body>
<div id="root"></div>
+1375 -1519
View File
File diff suppressed because it is too large Load Diff
+23 -27
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.33",
"version": "0.1.22",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,52 +13,48 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.9.13",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.5.7",
"@pandacss/preset-panda": "0.54.0",
"@react-aria/toast": "3.0.5",
"@livekit/components-react": "2.9.3",
"@livekit/components-styles": "1.1.5",
"@livekit/track-processors": "0.5.6",
"@pandacss/preset-panda": "0.53.6",
"@react-aria/toast": "3.0.2",
"@remixicon/react": "4.6.0",
"@tanstack/react-query": "5.81.5",
"@timephy/rnnoise-wasm": "1.0.0",
"@tanstack/react-query": "5.76.0",
"crisp-sdk-web": "1.0.25",
"hoofd": "1.7.3",
"humanize-duration": "3.33.0",
"i18next": "25.3.1",
"i18next-browser-languagedetector": "8.2.0",
"i18next": "25.1.2",
"i18next-browser-languagedetector": "8.1.0",
"i18next-parser": "9.3.0",
"i18next-resources-to-backend": "1.2.1",
"libphonenumber-js": "1.12.10",
"livekit-client": "2.15.2",
"posthog-js": "1.256.2",
"livekit-client": "2.11.4",
"posthog-js": "1.240.6",
"react": "18.3.1",
"react-aria-components": "1.10.1",
"react-aria-components": "1.8.0",
"react-dom": "18.3.1",
"react-i18next": "15.1.1",
"use-sound": "5.0.0",
"valtio": "2.1.5",
"wouter": "3.7.1"
"wouter": "3.7.0"
},
"devDependencies": {
"@pandacss/dev": "0.54.0",
"@tanstack/eslint-plugin-query": "5.81.2",
"@tanstack/react-query-devtools": "5.81.5",
"@types/humanize-duration": "3.27.4",
"@types/node": "22.16.0",
"@pandacss/dev": "0.53.6",
"@tanstack/eslint-plugin-query": "5.74.7",
"@tanstack/react-query-devtools": "5.76.0",
"@types/node": "22.15.17",
"@types/react": "18.3.12",
"@types/react-dom": "18.3.1",
"@typescript-eslint/eslint-plugin": "8.35.1",
"@typescript-eslint/parser": "8.35.1",
"@vitejs/plugin-react": "4.6.0",
"@typescript-eslint/eslint-plugin": "8.32.0",
"@typescript-eslint/parser": "8.32.0",
"@vitejs/plugin-react": "4.4.1",
"eslint": "8.57.0",
"eslint-config-prettier": "10.1.5",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.6",
"prettier": "3.6.2",
"postcss": "8.5.3",
"prettier": "3.5.3",
"typescript": "5.8.3",
"vite": "7.0.2",
"vite": "6.2.7",
"vite-tsconfig-paths": "5.1.4"
}
}
+3 -4
View File
@@ -190,6 +190,9 @@ const config: Config = {
fonts: {
sans: {
value: [
'Marianne',
'Source Sans',
'Source Sans fallback',
'ui-sans-serif',
'system-ui',
'-apple-system',
@@ -315,10 +318,6 @@ const config: Config = {
'subtle-text': { value: '{colors.red.700}' },
...pandaPreset.theme.tokens.colors.red,
},
alert: {
DEFAULT: { value: '{colors.blue.700}' },
notification: { value: '{colors.red.600}' },
},
success: {
DEFAULT: { value: '{colors.green.700}' },
hover: { value: '{colors.green.800}' },

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

-102
View File
@@ -1,102 +0,0 @@
<svg width="160" height="31" viewBox="0 0 160 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.12361 11.0152C4 11.6494 4 12.4305 4 13.7388V16.886C4 18.5092 4 19.3208 4.23607 20.0474C4.44495 20.6902 4.78662 21.282 5.23892 21.7844C5.7501 22.3521 6.45297 22.7579 7.85872 23.5695L10.5842 25.1431C11.8283 25.8614 12.5219 26.2618 13.1829 26.4545V16.7441C13.1829 16.2587 12.9178 15.812 12.4917 15.5796L4.12361 11.0152Z" fill="#202328"/>
<path d="M4.12361 11.0152C4 11.6494 4 12.4305 4 13.7388V16.886C4 18.5092 4 19.3208 4.23607 20.0474C4.44495 20.6902 4.78662 21.282 5.23892 21.7844C5.7501 22.3521 6.45297 22.7579 7.85872 23.5695L10.5842 25.1431C11.8283 25.8614 12.5219 26.2618 13.1829 26.4545V16.7441C13.1829 16.2587 12.9178 15.812 12.4917 15.5796L4.12361 11.0152Z" fill="url(#paint0_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M4.12361 11.0152C4 11.6494 4 12.4305 4 13.7388V16.886C4 18.5092 4 19.3208 4.23607 20.0474C4.44495 20.6902 4.78662 21.282 5.23892 21.7844C5.7501 22.3521 6.45297 22.7579 7.85872 23.5695L10.5842 25.1431C11.8283 25.8614 12.5219 26.2618 13.1829 26.4545V16.7441C13.1829 16.2587 12.9178 15.812 12.4917 15.5796L4.12361 11.0152Z" fill="#252525"/>
<path d="M26.9113 12.4414V17.5045C26.9113 18.2046 27.2155 18.8701 27.7448 19.3282L31.3889 22.4817C31.6022 22.6622 31.8236 22.8098 32.0532 22.9246C32.291 23.0312 32.5207 23.0845 32.7421 23.0845C33.2177 23.0845 33.5991 22.9287 33.8861 22.6171C34.1813 22.2972 34.3289 21.8831 34.3289 21.3746V8.58126C34.3289 8.07281 34.1813 7.66276 33.8861 7.35113C33.5991 7.0313 33.2177 6.87138 32.7421 6.87138C32.5207 6.87138 32.291 6.92469 32.0532 7.0313C31.8236 7.13791 31.6022 7.28552 31.3889 7.47414L27.7477 10.6152C27.2166 11.0734 26.9113 11.74 26.9113 12.4414Z" fill="#202328"/>
<path d="M26.9113 12.4414V17.5045C26.9113 18.2046 27.2155 18.8701 27.7448 19.3282L31.3889 22.4817C31.6022 22.6622 31.8236 22.8098 32.0532 22.9246C32.291 23.0312 32.5207 23.0845 32.7421 23.0845C33.2177 23.0845 33.5991 22.9287 33.8861 22.6171C34.1813 22.2972 34.3289 21.8831 34.3289 21.3746V8.58126C34.3289 8.07281 34.1813 7.66276 33.8861 7.35113C33.5991 7.0313 33.2177 6.87138 32.7421 6.87138C32.5207 6.87138 32.291 6.92469 32.0532 7.0313C31.8236 7.13791 31.6022 7.28552 31.3889 7.47414L27.7477 10.6152C27.2166 11.0734 26.9113 11.74 26.9113 12.4414Z" fill="url(#paint1_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M26.9113 12.4414V17.5045C26.9113 18.2046 27.2155 18.8701 27.7448 19.3282L31.3889 22.4817C31.6022 22.6622 31.8236 22.8098 32.0532 22.9246C32.291 23.0312 32.5207 23.0845 32.7421 23.0845C33.2177 23.0845 33.5991 22.9287 33.8861 22.6171C34.1813 22.2972 34.3289 21.8831 34.3289 21.3746V8.58126C34.3289 8.07281 34.1813 7.66276 33.8861 7.35113C33.5991 7.0313 33.2177 6.87138 32.7421 6.87138C32.5207 6.87138 32.291 6.92469 32.0532 7.0313C31.8236 7.13791 31.6022 7.28552 31.3889 7.47414L27.7477 10.6152C27.2166 11.0734 26.9113 11.74 26.9113 12.4414Z" fill="#252525"/>
<path d="M5.34145 9.20693C5.24012 9.15166 5.13386 9.11423 5.02627 9.09356C5.09416 9.0069 5.16509 8.92246 5.23897 8.8404C5.75015 8.27267 6.45303 7.86686 7.85877 7.05525L10.5843 5.48166C11.99 4.67005 12.6929 4.26424 13.4402 4.1054C14.1014 3.96487 14.7847 3.96487 15.4459 4.1054C16.1931 4.26424 16.896 4.67005 18.3017 5.48166L20.9951 7.03669C21.0059 7.04286 21.0166 7.04905 21.0273 7.05526C22.4331 7.86687 23.136 8.27268 23.6471 8.84041C24.0994 9.34274 24.4411 9.93452 24.65 10.5774C24.8861 11.304 24.8861 12.1156 24.8861 13.7388V16.886C24.8861 18.5092 24.8861 19.3208 24.65 20.0474C24.4411 20.6903 24.0994 21.282 23.6471 21.7844C23.136 22.3521 22.4331 22.7579 21.0274 23.5695C21.0154 23.5764 21.0032 23.5834 20.9912 23.5903L18.3017 25.1431C16.896 25.9547 16.1931 26.3605 15.4459 26.5194C15.4151 26.5259 15.3844 26.5321 15.3536 26.538L15.3536 16.7441C15.3536 15.4644 14.6546 14.2869 13.5311 13.6741L5.34145 9.20693Z" fill="#202328"/>
<path d="M5.34145 9.20693C5.24012 9.15166 5.13386 9.11423 5.02627 9.09356C5.09416 9.0069 5.16509 8.92246 5.23897 8.8404C5.75015 8.27267 6.45303 7.86686 7.85877 7.05525L10.5843 5.48166C11.99 4.67005 12.6929 4.26424 13.4402 4.1054C14.1014 3.96487 14.7847 3.96487 15.4459 4.1054C16.1931 4.26424 16.896 4.67005 18.3017 5.48166L20.9951 7.03669C21.0059 7.04286 21.0166 7.04905 21.0273 7.05526C22.4331 7.86687 23.136 8.27268 23.6471 8.84041C24.0994 9.34274 24.4411 9.93452 24.65 10.5774C24.8861 11.304 24.8861 12.1156 24.8861 13.7388V16.886C24.8861 18.5092 24.8861 19.3208 24.65 20.0474C24.4411 20.6903 24.0994 21.282 23.6471 21.7844C23.136 22.3521 22.4331 22.7579 21.0274 23.5695C21.0154 23.5764 21.0032 23.5834 20.9912 23.5903L18.3017 25.1431C16.896 25.9547 16.1931 26.3605 15.4459 26.5194C15.4151 26.5259 15.3844 26.5321 15.3536 26.538L15.3536 16.7441C15.3536 15.4644 14.6546 14.2869 13.5311 13.6741L5.34145 9.20693Z" fill="url(#paint2_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M5.34145 9.20693C5.24012 9.15166 5.13386 9.11423 5.02627 9.09356C5.09416 9.0069 5.16509 8.92246 5.23897 8.8404C5.75015 8.27267 6.45303 7.86686 7.85877 7.05525L10.5843 5.48166C11.99 4.67005 12.6929 4.26424 13.4402 4.1054C14.1014 3.96487 14.7847 3.96487 15.4459 4.1054C16.1931 4.26424 16.896 4.67005 18.3017 5.48166L20.9951 7.03669C21.0059 7.04286 21.0166 7.04905 21.0273 7.05526C22.4331 7.86687 23.136 8.27268 23.6471 8.84041C24.0994 9.34274 24.4411 9.93452 24.65 10.5774C24.8861 11.304 24.8861 12.1156 24.8861 13.7388V16.886C24.8861 18.5092 24.8861 19.3208 24.65 20.0474C24.4411 20.6903 24.0994 21.282 23.6471 21.7844C23.136 22.3521 22.4331 22.7579 21.0274 23.5695C21.0154 23.5764 21.0032 23.5834 20.9912 23.5903L18.3017 25.1431C16.896 25.9547 16.1931 26.3605 15.4459 26.5194C15.4151 26.5259 15.3844 26.5321 15.3536 26.538L15.3536 16.7441C15.3536 15.4644 14.6546 14.2869 13.5311 13.6741L5.34145 9.20693Z" fill="#252525"/>
<path d="M42.1726 21.992C41.8807 21.992 41.6441 21.7553 41.6441 21.4634V8.89637C41.6441 8.60445 41.8807 8.36781 42.1726 8.36781H43.8793C44.1712 8.36781 44.4078 8.60446 44.4078 8.89637V18.9527C44.4078 19.2446 44.6445 19.4812 44.9364 19.4812H49.0565C49.3484 19.4812 49.585 19.7179 49.585 20.0098V21.4634C49.585 21.7553 49.3484 21.992 49.0565 21.992H42.1726Z" fill="#202328"/>
<path d="M42.1726 21.992C41.8807 21.992 41.6441 21.7553 41.6441 21.4634V8.89637C41.6441 8.60445 41.8807 8.36781 42.1726 8.36781H43.8793C44.1712 8.36781 44.4078 8.60446 44.4078 8.89637V18.9527C44.4078 19.2446 44.6445 19.4812 44.9364 19.4812H49.0565C49.3484 19.4812 49.585 19.7179 49.585 20.0098V21.4634C49.585 21.7553 49.3484 21.992 49.0565 21.992H42.1726Z" fill="url(#paint3_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M42.1726 21.992C41.8807 21.992 41.6441 21.7553 41.6441 21.4634V8.89637C41.6441 8.60445 41.8807 8.36781 42.1726 8.36781H43.8793C44.1712 8.36781 44.4078 8.60446 44.4078 8.89637V18.9527C44.4078 19.2446 44.6445 19.4812 44.9364 19.4812H49.0565C49.3484 19.4812 49.585 19.7179 49.585 20.0098V21.4634C49.585 21.7553 49.3484 21.992 49.0565 21.992H42.1726Z" fill="#252525"/>
<path d="M54.3225 22.2839C52.3567 22.2839 51.0137 21.1551 51.0137 19.345C51.0137 17.8658 52.1621 16.7564 54.2641 16.4061L57.0119 15.9421C57.1559 15.9178 57.2614 15.7931 57.2614 15.647V15.647C57.2614 14.6155 56.4829 13.9537 55.354 13.9537C54.5968 13.9537 53.9746 14.2359 53.4873 14.7126C53.2754 14.9199 52.9415 14.9736 52.7059 14.7937L51.7352 14.0527C51.5072 13.8786 51.4574 13.5517 51.6513 13.3402C52.5387 12.3718 53.8483 11.7933 55.4319 11.7933C58.2735 11.7933 59.7332 13.4866 59.7332 15.647V21.4634C59.7332 21.7553 59.4966 21.992 59.2046 21.992H57.6592C57.4395 21.992 57.2614 21.8139 57.2614 21.5942V21.5942C57.2614 21.4074 57.0113 21.3103 56.8662 21.4279C56.2099 21.9598 55.2597 22.2839 54.3225 22.2839ZM53.4661 19.2282C53.4661 19.89 53.9916 20.3181 54.8285 20.3181C55.8951 20.3181 56.6837 19.849 57.1778 19.1406C57.2345 19.0594 57.2614 18.9616 57.2614 18.8625V17.9442C57.2614 17.7809 57.1148 17.6567 56.9537 17.6835L54.9258 18.0215C53.8943 18.1967 53.4661 18.6249 53.4661 19.2282Z" fill="#202328"/>
<path d="M54.3225 22.2839C52.3567 22.2839 51.0137 21.1551 51.0137 19.345C51.0137 17.8658 52.1621 16.7564 54.2641 16.4061L57.0119 15.9421C57.1559 15.9178 57.2614 15.7931 57.2614 15.647V15.647C57.2614 14.6155 56.4829 13.9537 55.354 13.9537C54.5968 13.9537 53.9746 14.2359 53.4873 14.7126C53.2754 14.9199 52.9415 14.9736 52.7059 14.7937L51.7352 14.0527C51.5072 13.8786 51.4574 13.5517 51.6513 13.3402C52.5387 12.3718 53.8483 11.7933 55.4319 11.7933C58.2735 11.7933 59.7332 13.4866 59.7332 15.647V21.4634C59.7332 21.7553 59.4966 21.992 59.2046 21.992H57.6592C57.4395 21.992 57.2614 21.8139 57.2614 21.5942V21.5942C57.2614 21.4074 57.0113 21.3103 56.8662 21.4279C56.2099 21.9598 55.2597 22.2839 54.3225 22.2839ZM53.4661 19.2282C53.4661 19.89 53.9916 20.3181 54.8285 20.3181C55.8951 20.3181 56.6837 19.849 57.1778 19.1406C57.2345 19.0594 57.2614 18.9616 57.2614 18.8625V17.9442C57.2614 17.7809 57.1148 17.6567 56.9537 17.6835L54.9258 18.0215C53.8943 18.1967 53.4661 18.6249 53.4661 19.2282Z" fill="url(#paint4_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M54.3225 22.2839C52.3567 22.2839 51.0137 21.1551 51.0137 19.345C51.0137 17.8658 52.1621 16.7564 54.2641 16.4061L57.0119 15.9421C57.1559 15.9178 57.2614 15.7931 57.2614 15.647V15.647C57.2614 14.6155 56.4829 13.9537 55.354 13.9537C54.5968 13.9537 53.9746 14.2359 53.4873 14.7126C53.2754 14.9199 52.9415 14.9736 52.7059 14.7937L51.7352 14.0527C51.5072 13.8786 51.4574 13.5517 51.6513 13.3402C52.5387 12.3718 53.8483 11.7933 55.4319 11.7933C58.2735 11.7933 59.7332 13.4866 59.7332 15.647V21.4634C59.7332 21.7553 59.4966 21.992 59.2046 21.992H57.6592C57.4395 21.992 57.2614 21.8139 57.2614 21.5942V21.5942C57.2614 21.4074 57.0113 21.3103 56.8662 21.4279C56.2099 21.9598 55.2597 22.2839 54.3225 22.2839ZM53.4661 19.2282C53.4661 19.89 53.9916 20.3181 54.8285 20.3181C55.8951 20.3181 56.6837 19.849 57.1778 19.1406C57.2345 19.0594 57.2614 18.9616 57.2614 18.8625V17.9442C57.2614 17.7809 57.1148 17.6567 56.9537 17.6835L54.9258 18.0215C53.8943 18.1967 53.4661 18.6249 53.4661 19.2282Z" fill="#252525"/>
<path d="M61.615 20.4798C61.4201 20.2758 61.4422 19.9537 61.6496 19.7624L62.8612 18.6448C63.0887 18.4351 63.4451 18.467 63.6604 18.6893C64.4215 19.4754 65.354 19.9289 66.3006 19.9289C67.3906 19.9289 68.0523 19.345 68.0523 18.3718C68.0523 15.8611 61.863 16.5423 61.863 11.9296C61.863 9.76915 63.6536 7.97855 66.2617 7.97855C68.1118 7.97855 69.5631 8.72827 70.6879 9.89556C70.8856 10.1008 70.8624 10.4269 70.6509 10.618L69.4293 11.7221C69.201 11.9285 68.847 11.8949 68.641 11.6661C67.9369 10.8845 67.1583 10.392 66.2812 10.392C65.308 10.392 64.6657 11.0148 64.6657 11.8322C64.6657 14.3041 70.855 13.6034 70.855 18.2745C70.8161 20.8437 68.8308 22.3812 66.3201 22.3812C64.2311 22.3812 62.7702 21.6888 61.615 20.4798Z" fill="#202328"/>
<path d="M61.615 20.4798C61.4201 20.2758 61.4422 19.9537 61.6496 19.7624L62.8612 18.6448C63.0887 18.4351 63.4451 18.467 63.6604 18.6893C64.4215 19.4754 65.354 19.9289 66.3006 19.9289C67.3906 19.9289 68.0523 19.345 68.0523 18.3718C68.0523 15.8611 61.863 16.5423 61.863 11.9296C61.863 9.76915 63.6536 7.97855 66.2617 7.97855C68.1118 7.97855 69.5631 8.72827 70.6879 9.89556C70.8856 10.1008 70.8624 10.4269 70.6509 10.618L69.4293 11.7221C69.201 11.9285 68.847 11.8949 68.641 11.6661C67.9369 10.8845 67.1583 10.392 66.2812 10.392C65.308 10.392 64.6657 11.0148 64.6657 11.8322C64.6657 14.3041 70.855 13.6034 70.855 18.2745C70.8161 20.8437 68.8308 22.3812 66.3201 22.3812C64.2311 22.3812 62.7702 21.6888 61.615 20.4798Z" fill="url(#paint5_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M61.615 20.4798C61.4201 20.2758 61.4422 19.9537 61.6496 19.7624L62.8612 18.6448C63.0887 18.4351 63.4451 18.467 63.6604 18.6893C64.4215 19.4754 65.354 19.9289 66.3006 19.9289C67.3906 19.9289 68.0523 19.345 68.0523 18.3718C68.0523 15.8611 61.863 16.5423 61.863 11.9296C61.863 9.76915 63.6536 7.97855 66.2617 7.97855C68.1118 7.97855 69.5631 8.72827 70.6879 9.89556C70.8856 10.1008 70.8624 10.4269 70.6509 10.618L69.4293 11.7221C69.201 11.9285 68.847 11.8949 68.641 11.6661C67.9369 10.8845 67.1583 10.392 66.2812 10.392C65.308 10.392 64.6657 11.0148 64.6657 11.8322C64.6657 14.3041 70.855 13.6034 70.855 18.2745C70.8161 20.8437 68.8308 22.3812 66.3201 22.3812C64.2311 22.3812 62.7702 21.6888 61.615 20.4798Z" fill="#252525"/>
<path d="M79.2618 17.749V12.7111C79.2618 12.4192 79.4985 12.1826 79.7904 12.1826H81.2245C81.5165 12.1826 81.7531 12.4192 81.7531 12.7111V17.6322C81.7531 20.5712 80.0404 22.3812 77.1598 22.3812C74.2793 22.3812 72.586 20.5712 72.586 17.6322V12.7111C72.586 12.4192 72.8226 12.1826 73.1145 12.1826H74.5487C74.8406 12.1826 75.0773 12.4192 75.0773 12.7111V17.749C75.0773 19.1698 75.8169 20.0457 77.1793 20.0457C78.5222 20.0457 79.2618 19.1698 79.2618 17.749Z" fill="#202328"/>
<path d="M79.2618 17.749V12.7111C79.2618 12.4192 79.4985 12.1826 79.7904 12.1826H81.2245C81.5165 12.1826 81.7531 12.4192 81.7531 12.7111V17.6322C81.7531 20.5712 80.0404 22.3812 77.1598 22.3812C74.2793 22.3812 72.586 20.5712 72.586 17.6322V12.7111C72.586 12.4192 72.8226 12.1826 73.1145 12.1826H74.5487C74.8406 12.1826 75.0773 12.4192 75.0773 12.7111V17.749C75.0773 19.1698 75.8169 20.0457 77.1793 20.0457C78.5222 20.0457 79.2618 19.1698 79.2618 17.749Z" fill="url(#paint6_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M79.2618 17.749V12.7111C79.2618 12.4192 79.4985 12.1826 79.7904 12.1826H81.2245C81.5165 12.1826 81.7531 12.4192 81.7531 12.7111V17.6322C81.7531 20.5712 80.0404 22.3812 77.1598 22.3812C74.2793 22.3812 72.586 20.5712 72.586 17.6322V12.7111C72.586 12.4192 72.8226 12.1826 73.1145 12.1826H74.5487C74.8406 12.1826 75.0773 12.4192 75.0773 12.7111V17.749C75.0773 19.1698 75.8169 20.0457 77.1793 20.0457C78.5222 20.0457 79.2618 19.1698 79.2618 17.749Z" fill="#252525"/>
<path d="M85.1511 10.4698C84.2753 10.4698 83.5357 9.73023 83.5357 8.85439C83.5357 7.97855 84.2753 7.23895 85.1511 7.23895C86.0269 7.23895 86.7471 7.97855 86.7471 8.85439C86.7471 9.73023 86.0269 10.4698 85.1511 10.4698ZM84.434 21.992C84.1421 21.992 83.9055 21.7553 83.9055 21.4634V12.7111C83.9055 12.4192 84.1421 12.1826 84.434 12.1826H85.8487C86.1406 12.1826 86.3773 12.4192 86.3773 12.7111V21.4634C86.3773 21.7553 86.1406 21.992 85.8487 21.992H84.434Z" fill="#202328"/>
<path d="M85.1511 10.4698C84.2753 10.4698 83.5357 9.73023 83.5357 8.85439C83.5357 7.97855 84.2753 7.23895 85.1511 7.23895C86.0269 7.23895 86.7471 7.97855 86.7471 8.85439C86.7471 9.73023 86.0269 10.4698 85.1511 10.4698ZM84.434 21.992C84.1421 21.992 83.9055 21.7553 83.9055 21.4634V12.7111C83.9055 12.4192 84.1421 12.1826 84.434 12.1826H85.8487C86.1406 12.1826 86.3773 12.4192 86.3773 12.7111V21.4634C86.3773 21.7553 86.1406 21.992 85.8487 21.992H84.434Z" fill="url(#paint7_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M85.1511 10.4698C84.2753 10.4698 83.5357 9.73023 83.5357 8.85439C83.5357 7.97855 84.2753 7.23895 85.1511 7.23895C86.0269 7.23895 86.7471 7.97855 86.7471 8.85439C86.7471 9.73023 86.0269 10.4698 85.1511 10.4698ZM84.434 21.992C84.1421 21.992 83.9055 21.7553 83.9055 21.4634V12.7111C83.9055 12.4192 84.1421 12.1826 84.434 12.1826H85.8487C86.1406 12.1826 86.3773 12.4192 86.3773 12.7111V21.4634C86.3773 21.7553 86.1406 21.992 85.8487 21.992H84.434Z" fill="#252525"/>
<path d="M89.3804 18.4108V14.9299C89.3804 14.638 89.1437 14.4014 88.8518 14.4014H88.0794C87.7875 14.4014 87.5508 14.1647 87.5508 13.8728V12.7111C87.5508 12.4192 87.7875 12.1826 88.0794 12.1826H88.8518C89.1437 12.1826 89.3804 11.9459 89.3804 11.654V10.2588C89.3804 9.96687 89.617 9.73023 89.9089 9.73023H91.3431C91.635 9.73023 91.8716 9.96687 91.8716 10.2588V11.654C91.8716 11.9459 92.1083 12.1826 92.4002 12.1826H94.3404C94.6323 12.1826 94.869 12.4192 94.869 12.7111V13.8728C94.869 14.1647 94.6323 14.4014 94.3404 14.4014H92.4002C92.1083 14.4014 91.8716 14.638 91.8716 14.9299V18.4108C91.8716 19.5007 92.4555 19.9289 93.4287 19.9289C93.6929 19.9289 93.922 19.9172 94.1227 19.8949C94.4967 19.8534 94.8884 20.1278 94.8884 20.5041V21.5303C94.8884 21.7399 94.7656 21.9334 94.5654 21.9955C94.1445 22.1261 93.6451 22.1866 93.0005 22.1866C90.5481 22.1866 89.3804 20.8047 89.3804 18.4108Z" fill="#202328"/>
<path d="M89.3804 18.4108V14.9299C89.3804 14.638 89.1437 14.4014 88.8518 14.4014H88.0794C87.7875 14.4014 87.5508 14.1647 87.5508 13.8728V12.7111C87.5508 12.4192 87.7875 12.1826 88.0794 12.1826H88.8518C89.1437 12.1826 89.3804 11.9459 89.3804 11.654V10.2588C89.3804 9.96687 89.617 9.73023 89.9089 9.73023H91.3431C91.635 9.73023 91.8716 9.96687 91.8716 10.2588V11.654C91.8716 11.9459 92.1083 12.1826 92.4002 12.1826H94.3404C94.6323 12.1826 94.869 12.4192 94.869 12.7111V13.8728C94.869 14.1647 94.6323 14.4014 94.3404 14.4014H92.4002C92.1083 14.4014 91.8716 14.638 91.8716 14.9299V18.4108C91.8716 19.5007 92.4555 19.9289 93.4287 19.9289C93.6929 19.9289 93.922 19.9172 94.1227 19.8949C94.4967 19.8534 94.8884 20.1278 94.8884 20.5041V21.5303C94.8884 21.7399 94.7656 21.9334 94.5654 21.9955C94.1445 22.1261 93.6451 22.1866 93.0005 22.1866C90.5481 22.1866 89.3804 20.8047 89.3804 18.4108Z" fill="url(#paint8_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M89.3804 18.4108V14.9299C89.3804 14.638 89.1437 14.4014 88.8518 14.4014H88.0794C87.7875 14.4014 87.5508 14.1647 87.5508 13.8728V12.7111C87.5508 12.4192 87.7875 12.1826 88.0794 12.1826H88.8518C89.1437 12.1826 89.3804 11.9459 89.3804 11.654V10.2588C89.3804 9.96687 89.617 9.73023 89.9089 9.73023H91.3431C91.635 9.73023 91.8716 9.96687 91.8716 10.2588V11.654C91.8716 11.9459 92.1083 12.1826 92.4002 12.1826H94.3404C94.6323 12.1826 94.869 12.4192 94.869 12.7111V13.8728C94.869 14.1647 94.6323 14.4014 94.3404 14.4014H92.4002C92.1083 14.4014 91.8716 14.638 91.8716 14.9299V18.4108C91.8716 19.5007 92.4555 19.9289 93.4287 19.9289C93.6929 19.9289 93.922 19.9172 94.1227 19.8949C94.4967 19.8534 94.8884 20.1278 94.8884 20.5041V21.5303C94.8884 21.7399 94.7656 21.9334 94.5654 21.9955C94.1445 22.1261 93.6451 22.1866 93.0005 22.1866C90.5481 22.1866 89.3804 20.8047 89.3804 18.4108Z" fill="#252525"/>
<path d="M104.937 20.0772C105.161 20.2547 105.205 20.581 105.01 20.7898C104.081 21.7826 102.7 22.3812 101.058 22.3812C97.5737 22.3812 95.6079 19.9483 95.6079 17.0873C95.6079 14.1873 97.4375 11.7933 100.649 11.7933C103.374 11.7933 105.164 13.6423 105.164 16.2114C105.164 16.5636 105.132 16.8995 105.088 17.1824C105.05 17.4248 104.835 17.5933 104.59 17.5933H98.4383C98.2791 17.5933 98.1553 17.7338 98.1856 17.8901C98.4992 19.5033 99.5865 20.1624 101.038 20.1624C101.891 20.1624 102.657 19.8659 103.2 19.391C103.423 19.1951 103.761 19.1468 103.994 19.3312L104.937 20.0772ZM100.59 13.8175C99.4333 13.8175 98.6344 14.4037 98.298 15.4868C98.2481 15.6473 98.3742 15.8027 98.5422 15.8027H102.457C102.607 15.8027 102.728 15.6774 102.703 15.5295C102.554 14.6358 101.845 13.8175 100.59 13.8175Z" fill="#202328"/>
<path d="M104.937 20.0772C105.161 20.2547 105.205 20.581 105.01 20.7898C104.081 21.7826 102.7 22.3812 101.058 22.3812C97.5737 22.3812 95.6079 19.9483 95.6079 17.0873C95.6079 14.1873 97.4375 11.7933 100.649 11.7933C103.374 11.7933 105.164 13.6423 105.164 16.2114C105.164 16.5636 105.132 16.8995 105.088 17.1824C105.05 17.4248 104.835 17.5933 104.59 17.5933H98.4383C98.2791 17.5933 98.1553 17.7338 98.1856 17.8901C98.4992 19.5033 99.5865 20.1624 101.038 20.1624C101.891 20.1624 102.657 19.8659 103.2 19.391C103.423 19.1951 103.761 19.1468 103.994 19.3312L104.937 20.0772ZM100.59 13.8175C99.4333 13.8175 98.6344 14.4037 98.298 15.4868C98.2481 15.6473 98.3742 15.8027 98.5422 15.8027H102.457C102.607 15.8027 102.728 15.6774 102.703 15.5295C102.554 14.6358 101.845 13.8175 100.59 13.8175Z" fill="url(#paint9_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M104.937 20.0772C105.161 20.2547 105.205 20.581 105.01 20.7898C104.081 21.7826 102.7 22.3812 101.058 22.3812C97.5737 22.3812 95.6079 19.9483 95.6079 17.0873C95.6079 14.1873 97.4375 11.7933 100.649 11.7933C103.374 11.7933 105.164 13.6423 105.164 16.2114C105.164 16.5636 105.132 16.8995 105.088 17.1824C105.05 17.4248 104.835 17.5933 104.59 17.5933H98.4383C98.2791 17.5933 98.1553 17.7338 98.1856 17.8901C98.4992 19.5033 99.5865 20.1624 101.038 20.1624C101.891 20.1624 102.657 19.8659 103.2 19.391C103.423 19.1951 103.761 19.1468 103.994 19.3312L104.937 20.0772ZM100.59 13.8175C99.4333 13.8175 98.6344 14.4037 98.298 15.4868C98.2481 15.6473 98.3742 15.8027 98.5422 15.8027H102.457C102.607 15.8027 102.728 15.6774 102.703 15.5295C102.554 14.6358 101.845 13.8175 100.59 13.8175Z" fill="#252525"/>
<path d="M112.698 21.992C112.406 21.992 112.169 21.7553 112.169 21.4634V8.89637C112.169 8.60445 112.406 8.36781 112.698 8.36781H114.207C114.392 8.36781 114.563 8.46433 114.659 8.62236L118.444 14.8655C118.547 15.0352 118.793 15.0352 118.896 14.8655L122.681 8.62236C122.777 8.46433 122.948 8.36781 123.133 8.36781H124.642C124.934 8.36781 125.171 8.60446 125.171 8.89637V21.4634C125.171 21.7553 124.934 21.992 124.642 21.992H123.87C123.578 21.992 123.341 21.7553 123.341 21.4634V11.7599C123.341 11.4929 122.99 11.3943 122.851 11.6221L119.525 17.0678C119.429 17.225 119.258 17.3208 119.074 17.3208H118.266C118.082 17.3208 117.911 17.225 117.815 17.0678L114.489 11.6221C114.349 11.3943 113.999 11.4929 113.999 11.7599V21.4634C113.999 21.7553 113.762 21.992 113.47 21.992H112.698Z" fill="#202328"/>
<path d="M112.698 21.992C112.406 21.992 112.169 21.7553 112.169 21.4634V8.89637C112.169 8.60445 112.406 8.36781 112.698 8.36781H114.207C114.392 8.36781 114.563 8.46433 114.659 8.62236L118.444 14.8655C118.547 15.0352 118.793 15.0352 118.896 14.8655L122.681 8.62236C122.777 8.46433 122.948 8.36781 123.133 8.36781H124.642C124.934 8.36781 125.171 8.60446 125.171 8.89637V21.4634C125.171 21.7553 124.934 21.992 124.642 21.992H123.87C123.578 21.992 123.341 21.7553 123.341 21.4634V11.7599C123.341 11.4929 122.99 11.3943 122.851 11.6221L119.525 17.0678C119.429 17.225 119.258 17.3208 119.074 17.3208H118.266C118.082 17.3208 117.911 17.225 117.815 17.0678L114.489 11.6221C114.349 11.3943 113.999 11.4929 113.999 11.7599V21.4634C113.999 21.7553 113.762 21.992 113.47 21.992H112.698Z" fill="url(#paint10_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M112.698 21.992C112.406 21.992 112.169 21.7553 112.169 21.4634V8.89637C112.169 8.60445 112.406 8.36781 112.698 8.36781H114.207C114.392 8.36781 114.563 8.46433 114.659 8.62236L118.444 14.8655C118.547 15.0352 118.793 15.0352 118.896 14.8655L122.681 8.62236C122.777 8.46433 122.948 8.36781 123.133 8.36781H124.642C124.934 8.36781 125.171 8.60446 125.171 8.89637V21.4634C125.171 21.7553 124.934 21.992 124.642 21.992H123.87C123.578 21.992 123.341 21.7553 123.341 21.4634V11.7599C123.341 11.4929 122.99 11.3943 122.851 11.6221L119.525 17.0678C119.429 17.225 119.258 17.3208 119.074 17.3208H118.266C118.082 17.3208 117.911 17.225 117.815 17.0678L114.489 11.6221C114.349 11.3943 113.999 11.4929 113.999 11.7599V21.4634C113.999 21.7553 113.762 21.992 113.47 21.992H112.698Z" fill="#252525"/>
<path d="M136.902 20.0585C137.129 20.2352 137.175 20.5635 136.98 20.7748C136.061 21.7688 134.73 22.3812 133.107 22.3812C129.798 22.3812 127.755 19.9483 127.755 17.0873C127.755 14.1094 129.662 11.7933 132.698 11.7933C135.365 11.7933 137.097 13.6618 137.097 16.0752C137.097 16.289 137.079 16.4968 137.054 16.686C137.02 16.933 136.802 17.1067 136.553 17.1067H129.719C129.57 17.1067 129.45 17.2297 129.461 17.3778C129.624 19.5703 131.087 20.8826 133.107 20.8826C134.162 20.8826 135.081 20.4767 135.73 19.8341C135.942 19.6246 136.279 19.5738 136.514 19.7567L136.902 20.0585ZM132.659 13.1947C131.147 13.1947 130.061 14.0304 129.641 15.5262C129.596 15.6875 129.722 15.8416 129.889 15.8416H135.167C135.318 15.8416 135.439 15.7164 135.42 15.5674C135.254 14.2514 134.28 13.1947 132.659 13.1947Z" fill="#202328"/>
<path d="M136.902 20.0585C137.129 20.2352 137.175 20.5635 136.98 20.7748C136.061 21.7688 134.73 22.3812 133.107 22.3812C129.798 22.3812 127.755 19.9483 127.755 17.0873C127.755 14.1094 129.662 11.7933 132.698 11.7933C135.365 11.7933 137.097 13.6618 137.097 16.0752C137.097 16.289 137.079 16.4968 137.054 16.686C137.02 16.933 136.802 17.1067 136.553 17.1067H129.719C129.57 17.1067 129.45 17.2297 129.461 17.3778C129.624 19.5703 131.087 20.8826 133.107 20.8826C134.162 20.8826 135.081 20.4767 135.73 19.8341C135.942 19.6246 136.279 19.5738 136.514 19.7567L136.902 20.0585ZM132.659 13.1947C131.147 13.1947 130.061 14.0304 129.641 15.5262C129.596 15.6875 129.722 15.8416 129.889 15.8416H135.167C135.318 15.8416 135.439 15.7164 135.42 15.5674C135.254 14.2514 134.28 13.1947 132.659 13.1947Z" fill="url(#paint11_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M136.902 20.0585C137.129 20.2352 137.175 20.5635 136.98 20.7748C136.061 21.7688 134.73 22.3812 133.107 22.3812C129.798 22.3812 127.755 19.9483 127.755 17.0873C127.755 14.1094 129.662 11.7933 132.698 11.7933C135.365 11.7933 137.097 13.6618 137.097 16.0752C137.097 16.289 137.079 16.4968 137.054 16.686C137.02 16.933 136.802 17.1067 136.553 17.1067H129.719C129.57 17.1067 129.45 17.2297 129.461 17.3778C129.624 19.5703 131.087 20.8826 133.107 20.8826C134.162 20.8826 135.081 20.4767 135.73 19.8341C135.942 19.6246 136.279 19.5738 136.514 19.7567L136.902 20.0585ZM132.659 13.1947C131.147 13.1947 130.061 14.0304 129.641 15.5262C129.596 15.6875 129.722 15.8416 129.889 15.8416H135.167C135.318 15.8416 135.439 15.7164 135.42 15.5674C135.254 14.2514 134.28 13.1947 132.659 13.1947Z" fill="#252525"/>
<path d="M147.784 20.0585C148.011 20.2352 148.057 20.5635 147.862 20.7748C146.943 21.7688 145.612 22.3812 143.989 22.3812C140.68 22.3812 138.637 19.9483 138.637 17.0873C138.637 14.1094 140.544 11.7933 143.58 11.7933C146.247 11.7933 147.979 13.6618 147.979 16.0752C147.979 16.289 147.961 16.4968 147.936 16.686C147.902 16.933 147.684 17.1067 147.435 17.1067H140.601C140.452 17.1067 140.332 17.2297 140.343 17.3778C140.506 19.5703 141.969 20.8826 143.989 20.8826C145.044 20.8826 145.963 20.4767 146.612 19.8341C146.824 19.6246 147.161 19.5739 147.396 19.7567L147.784 20.0585ZM143.541 13.1947C142.029 13.1947 140.943 14.0304 140.523 15.5262C140.478 15.6875 140.604 15.8416 140.771 15.8416H146.049C146.199 15.8416 146.32 15.7164 146.302 15.5674C146.136 14.2514 145.162 13.1947 143.541 13.1947Z" fill="#202328"/>
<path d="M147.784 20.0585C148.011 20.2352 148.057 20.5635 147.862 20.7748C146.943 21.7688 145.612 22.3812 143.989 22.3812C140.68 22.3812 138.637 19.9483 138.637 17.0873C138.637 14.1094 140.544 11.7933 143.58 11.7933C146.247 11.7933 147.979 13.6618 147.979 16.0752C147.979 16.289 147.961 16.4968 147.936 16.686C147.902 16.933 147.684 17.1067 147.435 17.1067H140.601C140.452 17.1067 140.332 17.2297 140.343 17.3778C140.506 19.5703 141.969 20.8826 143.989 20.8826C145.044 20.8826 145.963 20.4767 146.612 19.8341C146.824 19.6246 147.161 19.5739 147.396 19.7567L147.784 20.0585ZM143.541 13.1947C142.029 13.1947 140.943 14.0304 140.523 15.5262C140.478 15.6875 140.604 15.8416 140.771 15.8416H146.049C146.199 15.8416 146.32 15.7164 146.302 15.5674C146.136 14.2514 145.162 13.1947 143.541 13.1947Z" fill="url(#paint12_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M147.784 20.0585C148.011 20.2352 148.057 20.5635 147.862 20.7748C146.943 21.7688 145.612 22.3812 143.989 22.3812C140.68 22.3812 138.637 19.9483 138.637 17.0873C138.637 14.1094 140.544 11.7933 143.58 11.7933C146.247 11.7933 147.979 13.6618 147.979 16.0752C147.979 16.289 147.961 16.4968 147.936 16.686C147.902 16.933 147.684 17.1067 147.435 17.1067H140.601C140.452 17.1067 140.332 17.2297 140.343 17.3778C140.506 19.5703 141.969 20.8826 143.989 20.8826C145.044 20.8826 145.963 20.4767 146.612 19.8341C146.824 19.6246 147.161 19.5739 147.396 19.7567L147.784 20.0585ZM143.541 13.1947C142.029 13.1947 140.943 14.0304 140.523 15.5262C140.478 15.6875 140.604 15.8416 140.771 15.8416H146.049C146.199 15.8416 146.32 15.7164 146.302 15.5674C146.136 14.2514 145.162 13.1947 143.541 13.1947Z" fill="#252525"/>
<path d="M150.901 18.7027V14.2098C150.901 13.9179 150.664 13.6812 150.372 13.6812H149.58C149.288 13.6812 149.052 13.4446 149.052 13.1527V12.7111C149.052 12.4192 149.288 12.1826 149.58 12.1826H150.372C150.664 12.1826 150.901 11.9459 150.901 11.654V10.2588C150.901 9.96687 151.137 9.73023 151.429 9.73023H152.046C152.338 9.73023 152.575 9.96687 152.575 10.2588V11.654C152.575 11.9459 152.811 12.1826 153.103 12.1826H155.452C155.744 12.1826 155.981 12.4192 155.981 12.7111V13.1527C155.981 13.4446 155.744 13.6812 155.452 13.6812H153.103C152.811 13.6812 152.575 13.9179 152.575 14.2098V18.7027C152.575 20.2403 153.353 20.6685 154.54 20.6685C154.98 20.6685 155.328 20.6321 155.622 20.5592C155.806 20.5139 156 20.6439 156 20.833V21.5617C156 21.7751 155.873 21.9715 155.668 22.0302C155.282 22.1404 154.846 22.1866 154.307 22.1866C152.127 22.1866 150.901 21.0967 150.901 18.7027Z" fill="#202328"/>
<path d="M150.901 18.7027V14.2098C150.901 13.9179 150.664 13.6812 150.372 13.6812H149.58C149.288 13.6812 149.052 13.4446 149.052 13.1527V12.7111C149.052 12.4192 149.288 12.1826 149.58 12.1826H150.372C150.664 12.1826 150.901 11.9459 150.901 11.654V10.2588C150.901 9.96687 151.137 9.73023 151.429 9.73023H152.046C152.338 9.73023 152.575 9.96687 152.575 10.2588V11.654C152.575 11.9459 152.811 12.1826 153.103 12.1826H155.452C155.744 12.1826 155.981 12.4192 155.981 12.7111V13.1527C155.981 13.4446 155.744 13.6812 155.452 13.6812H153.103C152.811 13.6812 152.575 13.9179 152.575 14.2098V18.7027C152.575 20.2403 153.353 20.6685 154.54 20.6685C154.98 20.6685 155.328 20.6321 155.622 20.5592C155.806 20.5139 156 20.6439 156 20.833V21.5617C156 21.7751 155.873 21.9715 155.668 22.0302C155.282 22.1404 154.846 22.1866 154.307 22.1866C152.127 22.1866 150.901 21.0967 150.901 18.7027Z" fill="url(#paint13_linear_5970_3325)" fill-opacity="0.3"/>
<path d="M150.901 18.7027V14.2098C150.901 13.9179 150.664 13.6812 150.372 13.6812H149.58C149.288 13.6812 149.052 13.4446 149.052 13.1527V12.7111C149.052 12.4192 149.288 12.1826 149.58 12.1826H150.372C150.664 12.1826 150.901 11.9459 150.901 11.654V10.2588C150.901 9.96687 151.137 9.73023 151.429 9.73023H152.046C152.338 9.73023 152.575 9.96687 152.575 10.2588V11.654C152.575 11.9459 152.811 12.1826 153.103 12.1826H155.452C155.744 12.1826 155.981 12.4192 155.981 12.7111V13.1527C155.981 13.4446 155.744 13.6812 155.452 13.6812H153.103C152.811 13.6812 152.575 13.9179 152.575 14.2098V18.7027C152.575 20.2403 153.353 20.6685 154.54 20.6685C154.98 20.6685 155.328 20.6321 155.622 20.5592C155.806 20.5139 156 20.6439 156 20.833V21.5617C156 21.7751 155.873 21.9715 155.668 22.0302C155.282 22.1404 154.846 22.1866 154.307 22.1866C152.127 22.1866 150.901 21.0967 150.901 18.7027Z" fill="#252525"/>
<defs>
<linearGradient id="paint0_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint1_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint2_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint3_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint4_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint5_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint6_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint7_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint8_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint9_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint10_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint11_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint12_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
<linearGradient id="paint13_linear_5970_3325" x1="134.058" y1="8.24938" x2="134.058" y2="21.992" gradientUnits="userSpaceOnUse">
<stop stop-color="#C9191E" stop-opacity="0"/>
<stop offset="1" stop-color="#C9191E"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 33 KiB

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

-21
View File
@@ -10,37 +10,16 @@ export interface ApiConfig {
}
support?: {
id: string
help_article_transcript: string
help_article_recording: string
help_article_more_tools: string
}
feedback: {
url: string
}
transcript: {
form_beta_users: string
}
silence_livekit_debug_logs?: boolean
is_silent_login_enabled?: boolean
custom_css_url?: string
use_french_gov_footer?: boolean
use_proconnect_button?: boolean
recording?: {
is_enabled?: boolean
available_modes?: RecordingMode[]
expiration_days?: number
max_duration?: number
}
telephony: {
enabled: boolean
phone_number?: string
default_country?: string
}
manifest_link?: string
livekit: {
url: string
force_wss_protocol: boolean
enable_firefox_proxy_workaround: boolean
}
}
@@ -1,4 +1,4 @@
export const CameraIcon = () => {
export const VisioIcon = () => {
return (
<svg
width="24"

Before

Width:  |  Height:  |  Size: 651 KiB

After

Width:  |  Height:  |  Size: 651 KiB

Before

Width:  |  Height:  |  Size: 702 KiB

After

Width:  |  Height:  |  Size: 702 KiB

Before

Width:  |  Height:  |  Size: 599 KiB

After

Width:  |  Height:  |  Size: 599 KiB

Before

Width:  |  Height:  |  Size: 704 KiB

After

Width:  |  Height:  |  Size: 704 KiB

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M19.376 12.4161L8.77735 19.4818C8.54759 19.635 8.23715 19.5729 8.08397 19.3432C8.02922 19.261 8 19.1645 8 19.0658V4.93433C8 4.65818 8.22386 4.43433 8.5 4.43433C8.59871 4.43433 8.69522 4.46355 8.77735 4.5183L19.376 11.584C19.6057 11.7372 19.6678 12.0477 19.5146 12.2774C19.478 12.3323 19.4309 12.3795 19.376 12.4161Z"></path></svg>

After

Width:  |  Height:  |  Size: 419 B

@@ -2,33 +2,19 @@ import { silenceLiveKitLogs } from '@/utils/livekit'
import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
import { useSyncUserPreferencesWithBackend } from '@/features/auth'
import { useEffect } from 'react'
export const AppInitialization = () => {
const { data } = useConfig()
useSyncUserPreferencesWithBackend()
const {
analytics = {},
support = {},
silence_livekit_debug_logs = false,
custom_css_url = '',
} = data ?? {}
} = data || {}
useAnalytics(analytics)
useSupport(support)
useEffect(() => {
if (custom_css_url) {
const link = document.createElement('link')
link.href = custom_css_url
link.id = 'meet-custom-css'
link.rel = 'stylesheet'
document.head.appendChild(link)
}
}, [custom_css_url])
silenceLiveKitLogs(silence_livekit_debug_logs)
return null
+1 -1
View File
@@ -51,7 +51,7 @@ export const Avatar = ({
style,
...props
}: AvatarProps) => {
const initial = name?.trim()?.charAt(0) ?? ''
const initial = name?.trim()?.charAt(0) || ''
return (
<div
style={{
+1 -1
View File
@@ -14,7 +14,7 @@ export const ErrorScreen = ({
const { t } = useTranslation()
return (
<Screen layout="centered">
<CenteredContent title={title ?? t('error.heading')} withBackButton>
<CenteredContent title={title || t('error.heading')} withBackButton>
{!!body && (
<Center>
<Text as="p" variant="h3" centered>
@@ -14,8 +14,8 @@ export const FeedbackBanner = () => {
<div
className={css({
width: '100%',
backgroundColor: 'primary.50',
color: 'primary.600',
backgroundColor: '#E8EDFF',
color: '#0063CB',
display: { base: 'none', sm: 'flex' },
justifyContent: 'center',
padding: '0.5rem 0',
@@ -1,24 +0,0 @@
import { LinkButton } from '@/primitives'
import { authUrl } from '@/features/auth'
import { useTranslation } from 'react-i18next'
import { useConfig } from '@/api/useConfig'
import { ProConnectButton } from './ProConnectButton'
type LoginButtonProps = {
proConnectHint?: boolean // Hide hint in layouts where space doesn't allow it.
}
export const LoginButton = ({ proConnectHint = true }: LoginButtonProps) => {
const { t } = useTranslation('global', { keyPrefix: 'login' })
const { data } = useConfig()
if (data?.use_proconnect_button) {
return <ProConnectButton hint={proConnectHint} />
}
return (
<LinkButton href={authUrl()} data-attr="login" variant="primary">
{t('buttonLabel')}
</LinkButton>
)
}

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