Compare commits

..

1 Commits

Author SHA1 Message Date
lebaudantoine 93f8ce687c ♻️(backend) remove internationalization from non-user-facing strings
Remove translation markers from backend strings that are never displayed to
users. Streamlines localization process by focusing only on user-visible
content that requires actual translation.
2025-05-15 12:05:59 +02:00
92 changed files with 669 additions and 1708 deletions
+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 }}
+1 -1
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
+1 -1
View File
@@ -49,7 +49,7 @@ clean_old_images('localhost:5001/meet-summary')
# This is necessary because we need to inject the certificate into our LiveKit container
local_resource(
'copy-root-ca',
cmd='cp -f "$(mkcert -CAROOT)/rootCA.pem" ../docker/livekit/rootCA.pem',
cmd='cp "$(mkcert -CAROOT)/rootCA.pem" ../docker/livekit/rootCA.pem',
deps=[], # No dependencies needed
)
# Build a custom LiveKit Docker image that includes our root CA certificate
+91 -120
View File
@@ -2,7 +2,7 @@
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
## Prerequisites
- k8s cluster with an nginx-ingress controller
- an OIDC provider (if you don't have one, we will provide an example)
@@ -12,25 +12,14 @@ This document is a step-by-step guide that describes how to install Visio on a k
### Test cluster
If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**.
If you do not have a test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script **bin/start-kind.sh**.
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
To be able to use the script, you will need to install the following components:
To be able to use the script, you will need to install:
- Docker (https://docs.docker.com/desktop/)
- Kind (https://kind.sigs.k8s.io/docs/user/quick-start/#installation)
- Mkcert (https://github.com/FiloSottile/mkcert#installation)
- Helm (https://helm.sh/docs/intro/quickstart/#install-helm)
- kubectl (https://kubernetes.io/docs/tasks/tools/)
In order to initiate the local kind installation via **start-kind.sh** do the following:
1) Make sure administrator/root user context is able to execute mkcert, docker, kind etc. commands or the script might fail
2) Download the script to the device where the above components are installed
3) Make the script executable
4) Run the script with proper permissions (administrator/sudo etc.)
The output of the script will resemble the below example:
```
$ ./bin/start-kind.sh
@@ -110,7 +99,7 @@ When your k8s cluster is ready, you can start the deployment. This cluster is sp
Please remember that \*.127.0.0.1.nip.io will always resolve to 127.0.0.1, except in the k8s cluster where we configure CoreDNS to answer with the ingress-nginx service IP.
## Preparation of components
## Preparation
### What will you use to authenticate your users ?
@@ -122,7 +111,7 @@ If you haven't run the script **bin/start-kind.sh**, you'll need to manually cre
$ kubectl create namespace meet
```
If you have already run the script, you can skip this step and proceed to the next instruction. NOTE: Before you proceed, and is using the kind method, make sure you download this repo examples/ directory and its contents to the location where you will be executing the helm command. Helm will look for "examples/<name>values.yaml" from based on the path it is being executed.
If you have already run the script, you can skip this step and proceed to the next instruction.
```
$ kubectl config set-context --current --namespace=meet
@@ -243,118 +232,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 Visio 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. 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 |
| ----------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 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 |
| 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 |
| 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 |
| 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_ANALYTICS | Analytics information | {} |
| FRONTEND_SUPPORT | Crisp frontend support configuration | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
| 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 | [] |
| 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 |
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
| 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 | |
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
| 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 |
| 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
@@ -54,6 +54,3 @@ ALLOW_UNREGISTERED_ROOMS=False
# Recording
SCREEN_RECORDING_BASE_URL=http://localhost:3000/recordings
# Telephony
ROOM_TELEPHONY_ENABLED=True
+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")
+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):
+6 -20
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,7 +37,7 @@ 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 not self.instance.user == user
@@ -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.")
)
@@ -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")
+1
View File
@@ -1,3 +1,4 @@
# ruff: noqa: S311
"""
Core application factories
"""
@@ -1,45 +0,0 @@
# Generated by Django 5.1.9 on 2025-05-13 08:22
import secrets
from django.db import migrations, models
def generate_pin_for_rooms(apps, schema_editor):
"""Generate unique 10-digit PIN codes for existing rooms.
The PIN code is required for SIP telephony features.
"""
Room = apps.get_model('core', 'Room')
rooms_without_pin_code = Room.objects.filter(pin_code__isnull=True)
existing_pins = set(Room.objects.values_list('pin_code', flat=True))
length = 10
def generate_pin_code():
while True:
pin_code = str(secrets.randbelow(10 ** length)).zfill(length)
if not pin_code in existing_pins:
return pin_code
for room in rooms_without_pin_code:
room.pin_code = generate_pin_code()
room.save()
class Migration(migrations.Migration):
dependencies = [
('core', '0013_alter_user_language'),
]
operations = [
migrations.AddField(
model_name='room',
name='pin_code',
field=models.CharField(blank=True, help_text='Unique n-digit code that identifies this room in telephony mode.', max_length=None, null=True, unique=True, verbose_name='Room PIN code'),
),
migrations.RunPython(
generate_pin_for_rooms,
reverse_code=migrations.RunPython.noop
),
]
+9 -51
View File
@@ -2,7 +2,6 @@
Declare and configure the models for the Meet core application
"""
import secrets
import uuid
from datetime import datetime, timedelta
from logging import getLogger
@@ -15,6 +14,7 @@ from django.core import mail, validators
from django.core.exceptions import PermissionDenied, ValidationError
from django.db import models
from django.utils import timezone
from django.utils.functional import lazy
from django.utils.text import capfirst, slugify
from django.utils.translation import gettext_lazy as _
@@ -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):
@@ -164,7 +164,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
)
language = models.CharField(
max_length=10,
choices=settings.LANGUAGES,
choices=lazy(lambda: settings.LANGUAGES, tuple)(),
default=settings.LANGUAGE_CODE,
verbose_name=_("language"),
help_text=_("The language in which the user wants to see the interface."),
@@ -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."""
@@ -383,14 +383,6 @@ class Room(Resource):
verbose_name=_("Visio room configuration"),
help_text=_("Values for Visio parameters to configure the room."),
)
pin_code = models.CharField(
max_length=None,
unique=True,
blank=True,
null=True,
verbose_name=_("Room PIN code"),
help_text=_("Unique n-digit code that identifies this room in telephony mode."),
)
class Meta:
db_table = "meet_room"
@@ -401,14 +393,6 @@ class Room(Resource):
def __str__(self):
return capfirst(self.name)
def save(self, *args, **kwargs):
"""Generate a unique n-digit pin code for new rooms."""
if settings.ROOM_TELEPHONY_ENABLED and not self.pk and not self.pin_code:
self.pin_code = self.generate_unique_pin_code(
length=settings.ROOM_TELEPHONY_PIN_LENGTH
)
super().save(*args, **kwargs)
def clean_fields(self, exclude=None):
"""
Automatically generate the slug from the name and make sure it does not look like a UUID.
@@ -423,7 +407,6 @@ class Room(Resource):
pass
else:
raise ValidationError({"name": f'Room name "{self.name:s}" is reserved.'})
super().clean_fields(exclude=exclude)
@property
@@ -431,31 +414,6 @@ class Room(Resource):
"""Check if a room is public"""
return self.access_level == RoomAccessLevel.PUBLIC
@staticmethod
def generate_unique_pin_code(length):
"""Generate a unique n-digit PIN code"""
if length < 4:
raise ValueError(
"PIN code length must be at least 4 digits for minimal security"
)
max_value = 10**length
for _ in range(settings.ROOM_TELEPHONY_PIN_MAX_RETRIES):
pin_code = str(secrets.randbelow(max_value)).zfill(length)
if not Room.objects.filter(pin_code=pin_code).exists():
return pin_code
# Log a warning as a temporary measure until backend observability is implemented.
logger.warning(
"Failed to generate unique PIN code of length %s after %s attempts",
length,
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES,
)
return None
class BaseAccessManager(models.Manager):
"""Base manager for handling resource access control."""
+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
@@ -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):
@@ -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",
@@ -257,9 +257,7 @@ def test_request_entry_authenticated_user_public_room(settings):
with (
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,7 +310,7 @@ 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(LobbyService, "notify_participants", return_value=None),
@@ -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",
@@ -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"},
@@ -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."""
+6 -178
View File
@@ -2,12 +2,6 @@
Unit tests for the Room model
"""
# pylint: disable=W0613
import secrets
from logging import Logger
from unittest import mock
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
@@ -102,7 +96,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 +109,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 +122,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 +135,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 +148,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 +161,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
@@ -181,169 +175,3 @@ def test_models_rooms_is_public_property():
# Test non-public room
private_room = RoomFactory(access_level=RoomAccessLevel.RESTRICTED)
assert private_room.is_public is False
@mock.patch.object(Room, "generate_unique_pin_code")
def test_telephony_disabled_skips_pin_generation(
mock_generate_unique_pin_code, settings
):
"""Telephony disabled should not generate pin codes."""
settings.ROOM_TELEPHONY_ENABLED = False
room = RoomFactory()
mock_generate_unique_pin_code.assert_not_called()
assert room.pin_code is None
def test_default_and_custom_pin_length(settings):
"""Pin codes should be created with correct configured length."""
settings.ROOM_TELEPHONY_ENABLED = True
room = RoomFactory()
# Assert default value is 10 for collision reasons
assert len(room.pin_code) == 10
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
room = RoomFactory()
# Assert custom size
assert len(room.pin_code) == 5
def test_room_updates_preserve_pin_code(settings):
"""Room updates should preserve existing pin code."""
settings.ROOM_TELEPHONY_ENABLED = True
room = RoomFactory()
# Store the original pin code to compare after updates
previous_pin_code = room.pin_code
# If this method is called, it would indicate the pin is being regenerated unnecessarily
with mock.patch.object(
Room, "generate_unique_pin_code"
) as mock_generate_unique_pin_code:
# Explicitly call save to persist the changes to the room
room.slug = "aaa-aaaa-aaa"
room.save()
assert room.pin_code == previous_pin_code
mock_generate_unique_pin_code.assert_not_called()
@pytest.mark.parametrize("is_telephony_enabled", [True, False])
def test_manual_pin_code_updates(is_telephony_enabled, settings):
"""Manual pin code changes should persist regardless of telephony setting."""
settings.ROOM_TELEPHONY_ENABLED = is_telephony_enabled
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
room = RoomFactory()
assert room.pin_code != "12345"
room.pin_code = "12345"
room.save()
assert room.pin_code == "12345"
# No data validation when manual updates are made
room.pin_code = "123"
room.save()
assert room.pin_code == "123"
def test_pin_code_uniqueness(settings):
"""Duplicate pin codes should raise validation error."""
settings.ROOM_TELEPHONY_ENABLED = True
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
RoomFactory(pin_code="12345")
with pytest.raises(ValidationError) as excinfo:
RoomFactory(pin_code="12345")
assert "Room with this Room PIN code already exists." in str(excinfo.value)
@pytest.mark.parametrize("invalid_length", [0, 1, 2, 3])
def test_pin_code_minimum_length(invalid_length, settings):
"""Pin codes should enforce minimum length for security."""
settings.ROOM_TELEPHONY_ENABLED = True
settings.ROOM_TELEPHONY_PIN_LENGTH = 4
# Assert no exception is raised with a valid length
RoomFactory()
settings.ROOM_TELEPHONY_PIN_LENGTH = invalid_length
with pytest.raises(ValueError) as excinfo:
RoomFactory()
assert "PIN code length must be at least 4 digits for minimal security" in str(
excinfo.value
)
@mock.patch.object(Logger, "warning")
@mock.patch.object(secrets, "randbelow", return_value=12345)
def test_pin_generation_max_retries(mock_randbelow, mock_logger, settings):
"""Pin generation should give up after max retries."""
settings.ROOM_TELEPHONY_ENABLED = True
settings.ROOM_TELEPHONY_PIN_LENGTH = 5
RoomFactory(pin_code="12345")
# Assert default max retries is low, 5
room1 = RoomFactory()
assert mock_randbelow.call_count == 5
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
)
mock_logger.reset_mock()
mock_randbelow.reset_mock()
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES = 3
room2 = RoomFactory()
assert mock_randbelow.call_count == 3
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
)
@mock.patch.object(secrets, "randbelow", return_value=12345)
def test_pin_code_zero_padding(mock_randbelow, settings):
"""Pin codes should be zero-padded to meet required length."""
settings.ROOM_TELEPHONY_ENABLED = True
settings.ROOM_TELEPHONY_PIN_LENGTH = 10
room = RoomFactory()
assert room.pin_code == "0000012345"
@mock.patch.object(secrets, "randbelow", return_value=12345)
def test_pin_generation_upper_bound(mock_randbelow, settings):
"""Random number generator should use correct upper bound based on pin length."""
settings.ROOM_TELEPHONY_ENABLED = False
room = RoomFactory()
room.generate_unique_pin_code(length=5)
# Assert called with the right exclusive upper bound, 10^5
mock_randbelow.assert_called_with(100000)
Binary file not shown.
+14 -30
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"POT-Creation-Date: 2025-05-13 13:54+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,14 +29,6 @@ msgstr "Berechtigungen"
msgid "Important dates"
msgstr "Wichtige Daten"
#: core/admin.py:143
msgid "No owner"
msgstr "Kein Eigentümer"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Mehrere Eigentümer"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
@@ -245,27 +237,19 @@ msgstr "Visio-Raumkonfiguration"
msgid "Values for Visio parameters to configure the room."
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
#: core/models.py:391
msgid "Room PIN code"
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."
#: core/models.py:398 core/models.py:552
#: core/models.py:390 core/models.py:510
msgid "Room"
msgstr "Raum"
#: core/models.py:399
#: core/models.py:391
msgid "Rooms"
msgstr "Räume"
#: core/models.py:563
#: core/models.py:521
msgid "Worker ID"
msgstr "Worker-ID"
#: core/models.py:565
#: core/models.py:523
msgid ""
"Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking."
@@ -274,39 +258,39 @@ msgstr ""
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
"ermöglicht."
#: core/models.py:573
#: core/models.py:531
msgid "Recording mode"
msgstr "Aufzeichnungsmodus"
#: core/models.py:574
#: core/models.py:532
msgid "Defines the mode of recording being called."
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
#: core/models.py:580
#: core/models.py:538
msgid "Recording"
msgstr "Aufzeichnung"
#: core/models.py:581
#: core/models.py:539
msgid "Recordings"
msgstr "Aufzeichnungen"
#: core/models.py:689
#: core/models.py:647
msgid "Recording/user relation"
msgstr "Beziehung Aufzeichnung/Benutzer"
#: core/models.py:690
#: core/models.py:648
msgid "Recording/user relations"
msgstr "Beziehungen Aufzeichnung/Benutzer"
#: core/models.py:696
#: core/models.py:654
msgid "This user is already in this recording."
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
#: core/models.py:702
#: core/models.py:660
msgid "This team is already in this recording."
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
#: core/models.py:708
#: core/models.py:666
msgid "Either user or team must be set, not both."
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
Binary file not shown.
+14 -30
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"POT-Creation-Date: 2025-05-13 13:54+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,14 +29,6 @@ msgstr "Permissions"
msgid "Important dates"
msgstr "Important dates"
#: core/admin.py:143
msgid "No owner"
msgstr "No owner"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Multiple owners"
#: 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."
@@ -241,27 +233,19 @@ msgstr "Visio room configuration"
msgid "Values for Visio parameters to configure the room."
msgstr "Values for Visio parameters to configure the room."
#: core/models.py:391
msgid "Room PIN code"
msgstr "Room PIN code"
#: core/models.py:392
msgid "Unique n-digit code that identifies this room in telephony mode."
msgstr "Unique n-digit code that identifies this room in telephony mode."
#: core/models.py:398 core/models.py:552
#: core/models.py:390 core/models.py:510
msgid "Room"
msgstr "Room"
#: core/models.py:399
#: core/models.py:391
msgid "Rooms"
msgstr "Rooms"
#: core/models.py:563
#: core/models.py:521
msgid "Worker ID"
msgstr "Worker ID"
#: core/models.py:565
#: core/models.py:523
msgid ""
"Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking."
@@ -269,39 +253,39 @@ msgstr ""
"Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking."
#: core/models.py:573
#: core/models.py:531
msgid "Recording mode"
msgstr "Recording mode"
#: core/models.py:574
#: core/models.py:532
msgid "Defines the mode of recording being called."
msgstr "Defines the mode of recording being called."
#: core/models.py:580
#: core/models.py:538
msgid "Recording"
msgstr "Recording"
#: core/models.py:581
#: core/models.py:539
msgid "Recordings"
msgstr "Recordings"
#: core/models.py:689
#: core/models.py:647
msgid "Recording/user relation"
msgstr "Recording/user relation"
#: core/models.py:690
#: core/models.py:648
msgid "Recording/user relations"
msgstr "Recording/user relations"
#: core/models.py:696
#: core/models.py:654
msgid "This user is already in this recording."
msgstr "This user is already in this recording."
#: core/models.py:702
#: core/models.py:660
msgid "This team is already in this recording."
msgstr "This team is already in this recording."
#: core/models.py:708
#: core/models.py:666
msgid "Either user or team must be set, not both."
msgstr "Either user or team must be set, not both."
Binary file not shown.
+18 -34
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"POT-Creation-Date: 2025-05-13 13:54+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,14 +29,6 @@ msgstr "Permissions"
msgid "Important dates"
msgstr "Dates importantes"
#: core/admin.py:143
msgid "No owner"
msgstr "Pas de propriétaire"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Plusieurs propriétaires"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
@@ -138,12 +130,12 @@ 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, "
"Entrez un identifiant valide. Cette valeur ne peut contenir que des lettres, "
"des chiffres et les caractères @/./+/-/_."
#: core/models.py:144
msgid "sub"
msgstr "sub"
msgstr "identifiant"
#: core/models.py:146
msgid ""
@@ -246,68 +238,60 @@ msgstr "Configuration de la salle de visioconférence"
msgid "Values for Visio parameters to configure the room."
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
#: core/models.py:391
msgid "Room PIN code"
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."
#: core/models.py:398 core/models.py:552
#: core/models.py:390 core/models.py:510
msgid "Room"
msgstr "Salle"
#: core/models.py:399
#: core/models.py:391
msgid "Rooms"
msgstr "Salles"
#: core/models.py:563
#: core/models.py:521
msgid "Worker ID"
msgstr "ID du Worker"
#: core/models.py:565
#: core/models.py:523
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 "
"Entrez un identifiant pour l'enregistrement du travailleur. Cet identifiant "
"est conservé même lorsque le travailleur s'arrête, permettant un suivi "
"facile."
#: core/models.py:573
#: core/models.py:531
msgid "Recording mode"
msgstr "Mode d'enregistrement"
#: core/models.py:574
#: core/models.py:532
msgid "Defines the mode of recording being called."
msgstr "Définit le mode d'enregistrement appelé."
#: core/models.py:580
#: core/models.py:538
msgid "Recording"
msgstr "Enregistrement"
#: core/models.py:581
#: core/models.py:539
msgid "Recordings"
msgstr "Enregistrements"
#: core/models.py:689
#: core/models.py:647
msgid "Recording/user relation"
msgstr "Relation enregistrement/utilisateur"
#: core/models.py:690
#: core/models.py:648
msgid "Recording/user relations"
msgstr "Relations enregistrement/utilisateur"
#: core/models.py:696
#: core/models.py:654
msgid "This user is already in this recording."
msgstr "Cet utilisateur est déjà dans cet enregistrement."
#: core/models.py:702
#: core/models.py:660
msgid "This team is already in this recording."
msgstr "Cette équipe est déjà dans cet enregistrement."
#: core/models.py:708
#: core/models.py:666
msgid "Either user or team must be set, not both."
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
Binary file not shown.
+14 -30
View File
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"POT-Creation-Date: 2025-05-13 13:54+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,14 +29,6 @@ msgstr "Rechten"
msgid "Important dates"
msgstr "Belangrijke datums"
#: core/admin.py:143
msgid "No owner"
msgstr "Geen eigenaar"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Meerdere eigenaren"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
@@ -241,27 +233,19 @@ msgstr "Visio-ruimteconfiguratie"
msgid "Values for Visio parameters to configure the room."
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
#: core/models.py:391
msgid "Room PIN code"
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."
#: core/models.py:398 core/models.py:552
#: core/models.py:390 core/models.py:510
msgid "Room"
msgstr "Ruimte"
#: core/models.py:399
#: core/models.py:391
msgid "Rooms"
msgstr "Ruimtes"
#: core/models.py:563
#: core/models.py:521
msgid "Worker ID"
msgstr "Worker ID"
#: core/models.py:565
#: core/models.py:523
msgid ""
"Enter an identifier for the worker recording.This ID is retained even when "
"the worker stops, allowing for easy tracking."
@@ -269,39 +253,39 @@ msgstr ""
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
#: core/models.py:573
#: core/models.py:531
msgid "Recording mode"
msgstr "Opnamemodus"
#: core/models.py:574
#: core/models.py:532
msgid "Defines the mode of recording being called."
msgstr "Definieert de modus van opname die wordt aangeroepen."
#: core/models.py:580
#: core/models.py:538
msgid "Recording"
msgstr "Opname"
#: core/models.py:581
#: core/models.py:539
msgid "Recordings"
msgstr "Opnames"
#: core/models.py:689
#: core/models.py:647
msgid "Recording/user relation"
msgstr "Opname/gebruiker-relatie"
#: core/models.py:690
#: core/models.py:648
msgid "Recording/user relations"
msgstr "Opname/gebruiker-relaties"
#: core/models.py:696
#: core/models.py:654
msgid "This user is already in this recording."
msgstr "Deze gebruiker is al in deze opname."
#: core/models.py:702
#: core/models.py:660
msgid "This team is already in this recording."
msgstr "Dit team is al in deze opname."
#: core/models.py:708
#: core/models.py:666
msgid "Either user or team must be set, not both."
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
+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)
+15 -39
View File
@@ -11,7 +11,7 @@ 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 _
@@ -22,7 +22,7 @@ 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():
@@ -38,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
@@ -69,7 +69,7 @@ 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([])
@@ -106,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
@@ -166,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
@@ -176,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",
@@ -314,7 +314,7 @@ 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
@@ -340,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([])
@@ -360,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"
@@ -575,23 +573,6 @@ class Base(Configuration):
environ_prefix=None,
)
# SIP Telephony
ROOM_TELEPHONY_ENABLED = values.BooleanValue(
False,
environ_name="ROOM_TELEPHONY_ENABLED",
environ_prefix=None,
)
ROOM_TELEPHONY_PIN_LENGTH = values.PositiveIntegerValue(
10,
environ_name="ROOM_TELEPHONY_PIN_LENGTH",
environ_prefix=None,
)
ROOM_TELEPHONY_PIN_MAX_RETRIES = values.PositiveIntegerValue(
5,
environ_name="ROOM_TELEPHONY_PIN_MAX_RETRIES",
environ_prefix=None,
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
@@ -638,7 +619,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")
@@ -802,14 +786,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.26"
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==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 -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;
+35 -71
View File
@@ -1,28 +1,27 @@
{
"name": "meet",
"version": "0.1.26",
"version": "0.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.26",
"version": "0.1.22",
"dependencies": {
"@livekit/components-react": "2.9.12",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.5.7",
"@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.76.0",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.0.25",
"hoofd": "1.7.3",
"i18next": "25.1.2",
"i18next-browser-languagedetector": "8.1.0",
"i18next-parser": "9.3.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.13.8",
"livekit-client": "2.11.4",
"posthog-js": "1.240.6",
"react": "18.3.1",
"react-aria-components": "1.8.0",
@@ -406,9 +405,9 @@
}
},
"node_modules/@bufbuild/protobuf": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz",
"integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@clack/core": {
@@ -982,9 +981,9 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
@@ -1215,9 +1214,9 @@
}
},
"node_modules/@livekit/components-core": {
"version": "0.12.8",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.8.tgz",
"integrity": "sha512-ZqQ88DkZZw6h4XY/lFklOFsM76zZX0mIpa6HKxDgMgW3QpDjl7oOpQCHZYvaDhmJJ9X2m58oOCuf3RUdTKSJMA==",
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.4.tgz",
"integrity": "sha512-a/GkK8XFULPhXoSKxuXEU62gwTAYJ83DP5/vlRzwESEY+rsoiw2NvvPZtDCU17yyd/5QBIF9VdDjB9ZZF0dOfQ==",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/dom": "1.6.13",
@@ -1228,17 +1227,17 @@
"node": ">=18"
},
"peerDependencies": {
"livekit-client": "^2.13.3",
"livekit-client": "^2.11.1",
"tslib": "^2.6.2"
}
},
"node_modules/@livekit/components-react": {
"version": "2.9.12",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.12.tgz",
"integrity": "sha512-GSbVNEeJSGvjyRzUVHJvBahAvrC/zAG7gOD+UlgYnxjA1fEte4gSUtwbcdVauABGWZGtiaU2cQvSuNhCQaXRZQ==",
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.3.tgz",
"integrity": "sha512-gE1sEE57BkBz3+TQHrOXVDVwVMwV5wtIYokdrfd7vshh22/PtWWj3vON9wzYLFRKx98L6QyAzyh7W9EWu3Lj9Q==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/components-core": "0.12.8",
"@livekit/components-core": "0.12.4",
"clsx": "2.1.1",
"usehooks-ts": "3.1.1"
},
@@ -1247,7 +1246,7 @@
},
"peerDependencies": {
"@livekit/krisp-noise-filter": "^0.2.12",
"livekit-client": "^2.13.3",
"livekit-client": "^2.11.1",
"react": ">=18",
"react-dom": ">=18",
"tslib": "^2.6.2"
@@ -1259,9 +1258,9 @@
}
},
"node_modules/@livekit/components-styles": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@livekit/components-styles/-/components-styles-1.1.6.tgz",
"integrity": "sha512-V6zfuREC2ksW8z6T6WSbEvdLB5ICVikGz1GtLr59UcxHDyAsKDbuDHAyl3bF3xBqPKYmY3GWF3Qk39rnScyOtA==",
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@livekit/components-styles/-/components-styles-1.1.5.tgz",
"integrity": "sha512-SocIPcwm18S28zVruvJcmiHfbUIwGTfxGbUOIp1Db78EON/iJ2v7B2g/xD5sr+c7jXoV1DNUPl2qQiFW2S9dbw==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
@@ -1274,24 +1273,23 @@
"license": "Apache-2.0"
},
"node_modules/@livekit/protocol": {
"version": "1.39.2",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.39.2.tgz",
"integrity": "sha512-kYbIO/JlC6cylSxd4WJrBps9+zoZ9gifL7t3iW9whT8rbo5jHx03I4dwBLhzOonVyX+memSEO90m/ymNoT+aAw==",
"version": "1.36.1",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.36.1.tgz",
"integrity": "sha512-nN3QnITAQ5yXk7UKfotH7CRWIlEozNWeKVyFJ0/+dtSzvWP/ib+10l1DDnRYi3A1yICJOGAKFgJ5d6kmi1HCUA==",
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protobuf": "^1.10.0"
}
},
"node_modules/@livekit/track-processors": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.5.7.tgz",
"integrity": "sha512-/2SkuVAF+YiPNtOi9zQJz/yH1WGaK53XZ3PaESpLOiEYUBsYky13BrriXCXUf6kwn5R5+7ZsYWc2k3XSsAuLtg==",
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.5.6.tgz",
"integrity": "sha512-TlzObrSlp2PKor4VXqg6iefLRFVEb2T1lXwddBFdkPod60XVgYoMOj7V5xJm+UTE2MEtlE0003vUli9PyQGB1g==",
"license": "Apache-2.0",
"dependencies": {
"@mediapipe/tasks-vision": "0.10.14"
},
"peerDependencies": {
"@types/dom-mediacapture-transform": "^0.1.9",
"livekit-client": "^1.12.0 || ^2.1.0"
}
},
@@ -3889,11 +3887,6 @@
"react": "^18 || ^19"
}
},
"node_modules/@timephy/rnnoise-wasm": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@timephy/rnnoise-wasm/-/rnnoise-wasm-1.0.0.tgz",
"integrity": "sha512-zzRdFyALbhaNIuEo3LKazPWxabatbPxkaBrHzRjGDlVHX2dFklh68HUENHlvHkKsq+OOQSC1ag5sGPDMDgg2CA=="
},
"node_modules/@ts-morph/common": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.25.0.tgz",
@@ -3947,30 +3940,6 @@
"@babel/types": "^7.20.7"
}
},
"node_modules/@types/dom-mediacapture-record": {
"version": "1.0.22",
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.22.tgz",
"integrity": "sha512-mUMZLK3NvwRLcAAT9qmcK+9p7tpU2FHdDsntR3YI4+GY88XrgG4XiE7u1Q2LAN2/FZOz/tdMDC3GQCR4T8nFuw==",
"license": "MIT",
"peer": true
},
"node_modules/@types/dom-mediacapture-transform": {
"version": "0.1.11",
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-transform/-/dom-mediacapture-transform-0.1.11.tgz",
"integrity": "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/dom-webcodecs": "*"
}
},
"node_modules/@types/dom-webcodecs": {
"version": "0.1.15",
"resolved": "https://registry.npmjs.org/@types/dom-webcodecs/-/dom-webcodecs-0.1.15.tgz",
"integrity": "sha512-omOlCPvTWyPm4ZE5bZUhlSvnHM2ZWM2U+1cPiYFL/e8aV5O9MouELp+L4dMKNTON0nTeHqEg+KWDfFQMY5Wkaw==",
"license": "MIT",
"peer": true
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
@@ -7369,13 +7338,13 @@
}
},
"node_modules/livekit-client": {
"version": "2.13.8",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.13.8.tgz",
"integrity": "sha512-HXE89EizbgS/V+gh74GLGYOzvQ9d8qAsWt/N5lYkVGJX8PCq+7WK8whq5baFF7FBGkVPaE5NmW7Nk2V5nHvvWA==",
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.11.4.tgz",
"integrity": "sha512-V82NdyhHo3iBxxQmPTk95Fe2+VZMABWZL56t9oZmMgycoF8li8gs/KG7yyDsKki50EeKLgwOmlwqx/Xf0pc0+Q==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/mutex": "1.1.1",
"@livekit/protocol": "1.39.2",
"@livekit/protocol": "1.36.1",
"events": "^3.3.0",
"loglevel": "^1.9.2",
"sdp-transform": "^2.15.0",
@@ -7383,9 +7352,6 @@
"tslib": "2.8.1",
"typed-emitter": "^2.1.0",
"webrtc-adapter": "^9.0.1"
},
"peerDependencies": {
"@types/dom-mediacapture-record": "^1"
}
},
"node_modules/livekit-client/node_modules/loglevel": {
@@ -7419,8 +7385,7 @@
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
@@ -7453,7 +7418,6 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz",
"integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
+5 -6
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.26",
"version": "0.1.22",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,21 +13,20 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.9.12",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.5.7",
"@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.76.0",
"@timephy/rnnoise-wasm": "1.0.0",
"crisp-sdk-web": "1.0.25",
"hoofd": "1.7.3",
"i18next": "25.1.2",
"i18next-browser-languagedetector": "8.1.0",
"i18next-parser": "9.3.0",
"i18next-resources-to-backend": "1.2.1",
"livekit-client": "2.13.8",
"livekit-client": "2.11.4",
"posthog-js": "1.240.6",
"react": "18.3.1",
"react-aria-components": "1.8.0",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -2,11 +2,9 @@ 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'
export const AppInitialization = () => {
const { data } = useConfig()
useSyncUserPreferencesWithBackend()
const {
analytics = {},
@@ -2,5 +2,4 @@ export enum FeatureFlags {
Transcript = 'transcription-summary',
ScreenRecording = 'screen-recording',
faceLandmarks = 'face-landmarks',
noiseReduction = 'noise-reduction',
}
@@ -11,6 +11,5 @@ export const NotificationDuration = {
PARTICIPANT_JOINED: ToastDuration.LONG,
HAND_RAISED: ToastDuration.LONG,
LOWER_HAND: ToastDuration.EXTRA_LONG,
RECORDING_SAVING: ToastDuration.EXTRA_LONG,
REACTION_RECEIVED: ToastDuration.SHORT,
} as const
@@ -10,5 +10,4 @@ export enum NotificationType {
TranscriptionStopped = 'transcriptionStopped',
ScreenRecordingStarted = 'screenRecordingStarted',
ScreenRecordingStopped = 'screenRecordingStopped',
RecordingSaving = 'recordingSaving',
}
@@ -8,8 +8,6 @@ export interface ToastData {
participant: Participant
type: NotificationType
message?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
// Using a global queue for toasts allows for centralized management and queuing of notifications
@@ -1,62 +0,0 @@
import { useToast } from '@react-aria/toast'
import { useMemo, useRef } from 'react'
import { Text } from '@/primitives'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { useUser } from '@/features/auth'
import { css } from '@/styled-system/css'
import { RecordingMode } from '@/features/recording'
export function ToastRecordingSaving({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications', { keyPrefix: 'recordingSave' })
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const { user } = useUser()
const modeLabel = useMemo(() => {
const mode = props.toast.content.mode as RecordingMode
switch (mode) {
case RecordingMode.Transcript:
return 'transcript'
case RecordingMode.ScreenRecording:
return 'screenRecording'
}
}, [props.toast.content])
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={1}
>
<Text
margin={false}
className={css({
maxWidth: '22rem',
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'normal',
})}
>
{user?.email ? (
<span
dangerouslySetInnerHTML={{
__html: t(`${modeLabel}.message`, {
email: user.email,
}),
}}
/>
) : (
t(`${modeLabel}.default`)
)}
</Text>
</HStack>
</StyledToastContainer>
)
}
@@ -10,7 +10,6 @@ import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
import { ToastLowerHand } from './ToastLowerHand'
import { ToastAnyRecording } from './ToastAnyRecording'
import { ToastRecordingSaving } from './ToastRecordingSaving'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -44,11 +43,6 @@ const renderToast = (
case NotificationType.ScreenRecordingStopped:
return <ToastAnyRecording key={toast.key} toast={toast} state={state} />
case NotificationType.RecordingSaving:
return (
<ToastRecordingSaving key={toast.key} toast={toast} state={state} />
)
default:
return <Toast key={toast.key} toast={toast} state={state} />
}
@@ -1,3 +1,2 @@
export { useNotifyParticipants } from './hooks/useNotifyParticipants'
export { NotificationType } from './NotificationType'
export { notifyRecordingSaveInProgress } from './utils'
@@ -3,7 +3,6 @@ import { NotificationType } from './NotificationType'
import { NotificationDuration } from './NotificationDuration'
import { Participant } from 'livekit-client'
import { NotificationPayload } from './NotificationPayload'
import { RecordingMode } from '@/features/recording'
export const showLowerHandToast = (
participant: Participant,
@@ -50,17 +49,3 @@ export const decodeNotificationDataReceived = (
return
}
}
export const notifyRecordingSaveInProgress = (
mode: RecordingMode,
participant: Participant
) => {
toastQueue.add(
{
participant,
mode,
type: NotificationType.RecordingSaving,
},
{ timeout: NotificationDuration.RECORDING_SAVING }
)
}
@@ -106,10 +106,14 @@ export const RecordingStateToast = () => {
switch (recordingSnap.status) {
case RecordingStatus.TRANSCRIPT_STARTED:
return 'transcript.started'
case RecordingStatus.TRANSCRIPT_STOPPING:
return 'transcript.stopping'
case RecordingStatus.TRANSCRIPT_STARTING:
return 'transcript.starting'
case RecordingStatus.SCREEN_RECORDING_STARTED:
return 'screenRecording.started'
case RecordingStatus.SCREEN_RECORDING_STOPPING:
return 'screenRecording.stopping'
case RecordingStatus.SCREEN_RECORDING_STARTING:
return 'screenRecording.starting'
case RecordingStatus.ANY_STARTED:
@@ -18,7 +18,6 @@ import { CRISP_HELP_ARTICLE_RECORDING } from '@/utils/constants'
import {
NotificationType,
notifyRecordingSaveInProgress,
useNotifyParticipants,
} from '@/features/notifications'
import posthog from 'posthog-js'
@@ -85,10 +84,6 @@ export const ScreenRecordingSidePanel = () => {
await notifyParticipants({
type: NotificationType.ScreenRecordingStopped,
})
notifyRecordingSaveInProgress(
RecordingMode.ScreenRecording,
room.localParticipant
)
} else {
await startRecordingRoom({
id: roomId,
@@ -10,7 +10,6 @@ import {
useIsRecordingTransitioning,
useStartRecording,
useStopRecording,
useHasFeatureWithoutAdminRights,
} from '../index'
import { useEffect, useMemo, useState } from 'react'
import { ConnectionState, RoomEvent } from 'livekit-client'
@@ -24,7 +23,6 @@ import { FeatureFlags } from '@/features/analytics/enums'
import {
NotificationType,
useNotifyParticipants,
notifyRecordingSaveInProgress,
} from '@/features/notifications'
import posthog from 'posthog-js'
import { useSnapshot } from 'valtio/index'
@@ -44,12 +42,6 @@ export const TranscriptSidePanel = () => {
RecordingMode.Transcript,
FeatureFlags.Transcript
)
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
RecordingMode.Transcript,
FeatureFlags.Transcript
)
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
@@ -100,10 +92,6 @@ export const TranscriptSidePanel = () => {
await notifyParticipants({
type: NotificationType.TranscriptionStopped,
})
notifyRecordingSaveInProgress(
RecordingMode.Transcript,
room.localParticipant
)
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
@@ -146,54 +134,30 @@ export const TranscriptSidePanel = () => {
/>
{!hasTranscriptAccess ? (
<>
{hasFeatureWithoutAdminRights ? (
<>
<Text>{t('notAdminOrOwner.heading')}</Text>
<Text
variant="note"
wrap="balance"
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('notAdminOrOwner.body')}
<br />
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('notAdminOrOwner.linkMore')}
</A>
</Text>
</>
) : (
<>
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('beta.body')}{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={BETA_USERS_FORM_URL}
target="_blank"
>
{t('beta.button')}
</LinkButton>
</>
)}
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('beta.body')}{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={BETA_USERS_FORM_URL}
target="_blank"
>
{t('beta.button')}
</LinkButton>
</>
) : (
<>
@@ -1,22 +0,0 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { FeatureFlags } from '@/features/analytics/enums'
export const useHasFeatureWithoutAdminRights = (
mode: RecordingMode,
featureFlag: FeatureFlags
) => {
const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
isRecordingModeEnabled &&
!isAdminOrOwner
)
}
@@ -3,7 +3,6 @@ export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
export { useIsRecordingTransitioning } from './hooks/useIsRecordingTransitioning'
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
export { useIsRecordingActive } from './hooks/useIsRecordingActive'
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
// api
export { useStartRecording } from './api/startRecording'
@@ -15,8 +15,8 @@ import { InviteDialog } from './InviteDialog'
import { VideoConference } from '../livekit/prefabs/VideoConference'
import posthog from 'posthog-js'
import { css } from '@/styled-system/css'
import { LocalUserChoices } from '../routes/Room'
import { BackgroundProcessorFactory } from '../livekit/components/blur'
import { LocalUserChoices } from '@/stores/userChoices'
export const Conference = ({
roomId,
@@ -9,6 +9,7 @@ import { SelectToggleDevice } from '../livekit/components/controls/SelectToggleD
import { Field } from '@/primitives/Field'
import { Button, Dialog, Text, Form } from '@/primitives'
import { HStack, VStack } from '@/styled-system/jsx'
import { LocalUserChoices } from '../routes/Room'
import { Heading } from 'react-aria-components'
import { RiImageCircleAiFill } from '@remixicon/react'
import {
@@ -27,7 +28,6 @@ import { ApiLobbyStatus, ApiRequestEntry } from '../api/requestEntry'
import { Spinner } from '@/primitives/Spinner'
import { ApiAccessLevel } from '../api/ApiRoom'
import { useLoginHint } from '@/hooks/useLoginHint'
import { LocalUserChoices } from '@/stores/userChoices'
const onError = (e: Error) => console.error('ERROR', e)
@@ -116,25 +116,39 @@ export const Join = ({
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const {
userChoices: {
audioEnabled,
videoEnabled,
audioDeviceId,
videoDeviceId,
processorSerialized,
username,
},
saveAudioInputEnabled,
saveVideoInputEnabled,
userChoices: initialUserChoices,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
saveUsername,
saveProcessorSerialized,
} = usePersistentUserChoices()
} = usePersistentUserChoices({})
const [processor, setProcessor] = useState(
BackgroundProcessorFactory.deserializeProcessor(processorSerialized)
const [audioEnabled, setAudioEnabled] = useState(true)
const [videoEnabled, setVideoEnabled] = useState(true)
const [audioDeviceId, setAudioDeviceId] = useState<string>(
initialUserChoices.audioDeviceId
)
const [videoDeviceId, setVideoDeviceId] = useState<string>(
initialUserChoices.videoDeviceId
)
const [username, setUsername] = useState<string>(initialUserChoices.username)
const [processor, setProcessor] = useState(
BackgroundProcessorFactory.deserializeProcessor(
initialUserChoices.processorSerialized
)
)
useEffect(() => {
saveAudioInputDeviceId(audioDeviceId)
}, [audioDeviceId, saveAudioInputDeviceId])
useEffect(() => {
saveVideoInputDeviceId(videoDeviceId)
}, [videoDeviceId, saveVideoInputDeviceId])
useEffect(() => {
saveUsername(username)
}, [username, saveUsername])
useEffect(() => {
saveProcessorSerialized(processor?.serialize())
@@ -147,8 +161,8 @@ export const Join = ({
const tracks = usePreviewTracks(
{
audio: { deviceId: audioDeviceId },
video: { deviceId: videoDeviceId },
audio: { deviceId: initialUserChoices.audioDeviceId },
video: { deviceId: initialUserChoices.videoDeviceId },
},
onError
)
@@ -337,9 +351,9 @@ export const Join = ({
</H>
<Field
type="text"
onChange={saveUsername}
onChange={setUsername}
label={t('usernameLabel')}
defaultValue={username}
defaultValue={initialUserChoices?.username}
validate={(value) => !value && t('errors.usernameEmpty')}
wrapperProps={{
noMargin: true,
@@ -460,11 +474,11 @@ export const Join = ({
source={Track.Source.Microphone}
initialState={audioEnabled}
track={audioTrack}
initialDeviceId={audioDeviceId}
onChange={(enabled) => saveAudioInputEnabled(enabled)}
initialDeviceId={initialUserChoices.audioDeviceId}
onChange={(enabled) => setAudioEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
onActiveDeviceChange={(deviceId) =>
saveAudioInputDeviceId(deviceId ?? '')
setAudioDeviceId(deviceId ?? '')
}
variant="tertiary"
/>
@@ -472,11 +486,11 @@ export const Join = ({
source={Track.Source.Camera}
initialState={videoEnabled}
track={videoTrack}
initialDeviceId={videoDeviceId}
onChange={(enabled) => saveVideoInputEnabled(enabled)}
initialDeviceId={initialUserChoices.videoDeviceId}
onChange={(enabled) => setVideoEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
onActiveDeviceChange={(deviceId) =>
saveVideoInputDeviceId(deviceId ?? '')
setVideoDeviceId(deviceId ?? '')
}
variant="tertiary"
/>
@@ -23,7 +23,6 @@ export enum Emoji {
LAUGHING = 'face-with-tears-of-joy',
SURPRISED = 'face-with-open-mouth',
CELEBRATION = 'party-popper',
PLEASE = 'folded-hands',
}
export interface Reaction {
@@ -122,7 +121,7 @@ export const ReactionsToggle = () => {
className={css({
position: 'absolute',
top: -63,
left: -162,
left: -139,
borderRadius: '8px',
padding: '0.35rem',
backgroundColor: 'primaryDark.50',
@@ -99,7 +99,7 @@ export const SelectToggleDevice = <T extends ToggleSource>({
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const trackProps = useTrackToggle(props)
const { userChoices } = usePersistentUserChoices()
const { userChoices } = usePersistentUserChoices({})
const toggle = () => {
if (props.source === Track.Source.Camera) {
@@ -1,61 +0,0 @@
import { useRoomContext } from '@livekit/components-react'
import { useEffect, useRef } from 'react'
import { DisconnectReason, RoomEvent } from 'livekit-client'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import posthog from 'posthog-js'
export const useConnectionObserver = () => {
const room = useRoomContext()
const connectionStartTimeRef = useRef<number | null>(null)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
useEffect(() => {
if (!isAnalyticsEnabled) return
const handleConnection = () => {
// Preserve original connection timestamp across reconnections to measure
// total session duration from first connect to final disconnect.
if (connectionStartTimeRef.current != null) return
connectionStartTimeRef.current = Date.now()
posthog.capture('connection-event')
}
const handleReconnect = () => {
posthog.capture('reconnect-event')
}
const handleDisconnect = (
disconnectReason: DisconnectReason | undefined
) => {
const connectionEndTime = Date.now()
posthog.capture('disconnect-event', {
// Calculate total session duration from first connection to final disconnect
// This duration is sensitive to refreshing the page.
sessionDuration: connectionStartTimeRef.current
? connectionEndTime - connectionStartTimeRef.current
: -1,
reason: disconnectReason
? DisconnectReason[disconnectReason]
: 'UNKNOWN',
})
}
room.on(RoomEvent.Connected, handleConnection)
room.on(RoomEvent.Disconnected, handleDisconnect)
room.on(RoomEvent.Reconnecting, handleReconnect)
return () => {
room.off(RoomEvent.Connected, handleConnection)
room.off(RoomEvent.Disconnected, handleDisconnect)
room.off(RoomEvent.Reconnecting, handleReconnect)
}
}, [room, isAnalyticsEnabled])
useEffect(() => {
return () => {
connectionStartTimeRef.current = null
}
}, [])
}
@@ -1,32 +0,0 @@
import { useEffect } from 'react'
import { Track } from 'livekit-client'
import { useRoomContext } from '@livekit/components-react'
import { RnnNoiseProcessor } from '../processors/RnnNoiseProcessor'
import { usePersistentUserChoices } from './usePersistentUserChoices'
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
export const useNoiseReduction = () => {
const room = useRoomContext()
const noiseReductionAvailable = useNoiseReductionAvailable()
const {
userChoices: { noiseReductionEnabled },
} = usePersistentUserChoices()
const audioTrack = room.localParticipant.getTrackPublication(
Track.Source.Microphone
)?.audioTrack
useEffect(() => {
if (!audioTrack || !noiseReductionAvailable) return
const processor = audioTrack?.getProcessor()
if (noiseReductionEnabled && !processor) {
const rnnNoiseProcessor = new RnnNoiseProcessor()
audioTrack.setProcessor(rnnNoiseProcessor)
} else if (!noiseReductionEnabled && processor) {
audioTrack.stopProcessor()
}
}, [audioTrack, noiseReductionEnabled, noiseReductionAvailable])
}
@@ -1,13 +0,0 @@
import { useIsMobile } from '@/utils/useIsMobile'
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { FeatureFlags } from '@/features/analytics/enums'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
export const useNoiseReductionAvailable = () => {
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.faceLandmarks)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isMobile = useIsMobile()
return !isMobile && (!isAnalyticsEnabled || featureEnabled)
}
@@ -1,34 +1,71 @@
import { useSnapshot } from 'valtio'
import { userChoicesStore } from '@/stores/userChoices'
import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur'
import { UsePersistentUserChoicesOptions } from '@livekit/components-react'
import React from 'react'
import { LocalUserChoices } from '../../routes/Room'
import { saveUserChoices, loadUserChoices } from '@livekit/components-core'
import { ProcessorSerialized } from '../components/blur'
export function usePersistentUserChoices() {
const userChoicesSnap = useSnapshot(userChoicesStore)
/**
* From @livekit/component-react
*
* A hook that provides access to user choices stored in local storage, such as
* selected media devices and their current state (on or off), as well as the user name.
* @alpha
*/
export function usePersistentUserChoices(
options: UsePersistentUserChoicesOptions = {}
) {
const [userChoices, setSettings] = React.useState<LocalUserChoices>(
loadUserChoices(options.defaults, options.preventLoad ?? false)
)
const saveAudioInputEnabled = React.useCallback((isEnabled: boolean) => {
setSettings((prev: LocalUserChoices) => ({
...prev,
audioEnabled: isEnabled,
}))
}, [])
const saveVideoInputEnabled = React.useCallback((isEnabled: boolean) => {
setSettings((prev: LocalUserChoices) => ({
...prev,
videoEnabled: isEnabled,
}))
}, [])
const saveAudioInputDeviceId = React.useCallback((deviceId: string) => {
setSettings((prev: LocalUserChoices) => ({
...prev,
audioDeviceId: deviceId,
}))
}, [])
const saveVideoInputDeviceId = React.useCallback((deviceId: string) => {
setSettings((prev: LocalUserChoices) => ({
...prev,
videoDeviceId: deviceId,
}))
}, [])
const saveUsername = React.useCallback((username: string) => {
setSettings((prev: LocalUserChoices) => ({ ...prev, username: username }))
}, [])
const saveProcessorSerialized = React.useCallback(
(processorSerialized?: ProcessorSerialized) => {
setSettings((prev: LocalUserChoices) => ({
...prev,
processorSerialized,
}))
},
[]
)
React.useEffect(() => {
saveUserChoices(userChoices, options.preventSave ?? false)
}, [userChoices, options.preventSave])
return {
userChoices: userChoicesSnap,
saveAudioInputEnabled: (isEnabled: boolean) => {
userChoicesStore.audioEnabled = isEnabled
},
saveVideoInputEnabled: (isEnabled: boolean) => {
userChoicesStore.videoEnabled = isEnabled
},
saveAudioInputDeviceId: (deviceId: string) => {
userChoicesStore.audioDeviceId = deviceId
},
saveVideoInputDeviceId: (deviceId: string) => {
userChoicesStore.videoDeviceId = deviceId
},
saveUsername: (username: string) => {
userChoicesStore.username = username
},
saveNoiseReductionEnabled: (enabled: boolean) => {
userChoicesStore.noiseReductionEnabled = enabled
},
saveProcessorSerialized: (
processorSerialized: ProcessorSerialized | undefined
) => {
userChoicesStore.processorSerialized = processorSerialized
},
userChoices,
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
saveUsername,
saveProcessorSerialized,
}
}
@@ -4,7 +4,6 @@ import {
formatChatMessageLinks,
useChat,
useParticipants,
useRoomContext,
} from '@livekit/components-react'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
@@ -13,7 +12,6 @@ import { Div, Text } from '@/primitives'
import { ChatInput } from '../components/chat/Input'
import { ChatEntry } from '../components/chat/Entry'
import { useSidePanel } from '../hooks/useSidePanel'
import { LocalParticipant, RemoteParticipant, RoomEvent } from 'livekit-client'
export interface ChatProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -29,7 +27,6 @@ export function Chat({ ...props }: ChatProps) {
const inputRef = React.useRef<HTMLTextAreaElement>(null)
const ulRef = React.useRef<HTMLUListElement>(null)
const room = useRoomContext()
const { send, chatMessages, isSending } = useChat()
const { isChatOpen } = useSidePanel()
@@ -39,7 +36,6 @@ export function Chat({ ...props }: ChatProps) {
const participants = useParticipants()
const lastReadMsgAt = React.useRef<ChatMessage['timestamp']>(0)
const previousMessageCount = React.useRef(0)
async function handleSubmit(text: string) {
if (!send || !text) return
@@ -47,19 +43,6 @@ export function Chat({ ...props }: ChatProps) {
inputRef?.current?.focus()
}
// TEMPORARY: This is a brittle workaround that relies on message count tracking
// due to recent LiveKit useChat changes breaking the previous implementation
// (see https://github.com/livekit/components-js/issues/1158)
// Remove this once we refactor chat to use the new text stream approach
React.useEffect(() => {
if (!chatMessages || chatMessages.length <= previousMessageCount.current)
return
const msg = chatMessages.slice(-1)[0]
const from = msg.from as RemoteParticipant | LocalParticipant | undefined
room.emit(RoomEvent.ChatMessage, msg, from)
previousMessageCount.current = chatMessages.length
}, [chatMessages, room])
React.useEffect(() => {
if (chatMessages.length > 0 && ulRef.current) {
ulRef.current?.scrollTo({ top: ulRef.current.scrollHeight })
@@ -47,13 +47,16 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
* ```
* @public
*/
export function ControlBar({ onDeviceError }: ControlBarProps) {
export function ControlBar({
saveUserChoices = true,
onDeviceError,
}: ControlBarProps) {
const {
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
} = usePersistentUserChoices()
} = usePersistentUserChoices({ preventSave: !saveUserChoices })
const microphoneOnChange = React.useCallback(
(enabled: boolean, isUserInitiated: boolean) =>
@@ -30,8 +30,6 @@ import { SidePanel } from '../components/SidePanel'
import { useSidePanel } from '../hooks/useSidePanel'
import { RecordingStateToast } from '@/features/recording'
import { ScreenShareErrorModal } from '../components/ScreenShareErrorModal'
import { useConnectionObserver } from '../hooks/useConnectionObserver'
import { useNoiseReduction } from '../hooks/useNoiseReduction'
const LayoutWrapper = styled(
'div',
@@ -76,8 +74,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const lastAutoFocusedScreenShareTrack =
React.useRef<TrackReferenceOrPlaceholder | null>(null)
useConnectionObserver()
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
@@ -88,8 +84,6 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const layoutContext = useCreateLayoutContext()
useNoiseReduction()
const screenShareTracks = tracks
.filter(isTrackReference)
.filter((track) => track.publication.source === Track.Source.ScreenShare)
@@ -1,102 +0,0 @@
import { Track, TrackProcessor, ProcessorOptions } from 'livekit-client'
import { NoiseSuppressorWorklet_Name } from '@timephy/rnnoise-wasm'
// This is an example how to get the script path using Vite, may be different when using other build tools
// NOTE: `?worker&url` is important (`worker` to generate a working script, `url` to get its url to load it)
import NoiseSuppressorWorklet from '@timephy/rnnoise-wasm/NoiseSuppressorWorklet?worker&url'
// Use Jitsi's approach: maintain a global AudioContext variable
// and suspend/resume it as needed to manage audio state
let audioContext: AudioContext
export interface AudioProcessorInterface
extends TrackProcessor<Track.Kind.Audio> {
name: string
}
export class RnnNoiseProcessor implements AudioProcessorInterface {
name: string = 'noise-reduction'
processedTrack?: MediaStreamTrack
private source?: MediaStreamTrack
private sourceNode?: MediaStreamAudioSourceNode
private destinationNode?: MediaStreamAudioDestinationNode
private noiseSuppressionNode?: AudioWorkletNode
constructor() {}
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
if (!opts.track) {
throw new Error('Track is required for audio processing')
}
this.source = opts.track as MediaStreamTrack
if (!audioContext) {
audioContext = new AudioContext()
} else {
await audioContext.resume()
}
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
this.sourceNode = audioContext.createMediaStreamSource(
new MediaStream([this.source])
)
this.noiseSuppressionNode = new AudioWorkletNode(
audioContext,
NoiseSuppressorWorklet_Name
)
this.destinationNode = audioContext.createMediaStreamDestination()
// Connect the audio processing chain
this.sourceNode
.connect(this.noiseSuppressionNode)
.connect(this.destinationNode)
// Get the processed track
const tracks = this.destinationNode.stream.getAudioTracks()
if (tracks.length === 0) {
throw new Error('No audio tracks found for processing')
}
this.processedTrack = tracks[0]
}
async restart(opts: ProcessorOptions<Track.Kind.Audio>) {
await this.destroy()
return this.init(opts)
}
async destroy() {
// Clean up audio nodes and context
this.sourceNode?.disconnect()
this.noiseSuppressionNode?.disconnect()
this.destinationNode?.disconnect()
/**
* Audio Context Lifecycle Management
*
* We prefer suspending the audio context rather than destroying and recreating it
* to avoid memory leaks in WebAssembly-based audio processing.
*
* Issue: When an AudioContext containing WebAssembly modules is destroyed,
* the WASM resources are not properly garbage collected. This causes:
* - Retained JavaScript VM instances
* - Growing memory consumption over multiple create/destroy cycles
* - Potential performance degradation
*
* Solution: Use suspend() and resume() methods instead of close() to maintain
* the same context instance while controlling audio processing state.
*/
await audioContext.suspend()
this.sourceNode = undefined
this.destinationNode = undefined
this.source = undefined
this.processedTrack = undefined
this.noiseSuppressionNode = undefined
}
}
@@ -1,16 +1,23 @@
import { useEffect, useState } from 'react'
import { usePersistentUserChoices } from '@livekit/components-react'
import {
usePersistentUserChoices,
type LocalUserChoices as LocalUserChoicesLK,
} from '@livekit/components-react'
import { useLocation, useParams } from 'wouter'
import { ErrorScreen } from '@/components/ErrorScreen'
import { useUser, UserAware } from '@/features/auth'
import { Conference } from '../components/Conference'
import { Join } from '../components/Join'
import { useKeyboardShortcuts } from '@/features/shortcuts/useKeyboardShortcuts'
import { ProcessorSerialized } from '../livekit/components/blur'
import {
isRoomValid,
normalizeRoomId,
} from '@/features/rooms/utils/isRoomValid'
import { LocalUserChoices } from '@/stores/userChoices'
export type LocalUserChoices = LocalUserChoicesLK & {
processorSerialized?: ProcessorSerialized
}
export const Room = () => {
const { isLoggedIn } = useUser()
@@ -1,4 +1,4 @@
import { DialogProps, Field, H, Switch } from '@/primitives'
import { DialogProps, Field, H } from '@/primitives'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import {
@@ -11,48 +11,17 @@ import { useTranslation } from 'react-i18next'
import { SoundTester } from '@/components/SoundTester'
import { HStack } from '@/styled-system/jsx'
import { ActiveSpeaker } from '@/features/rooms/components/ActiveSpeaker'
import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePersistentUserChoices'
import { ReactNode } from 'react'
import { css } from '@/styled-system/css'
import posthog from 'posthog-js'
import { useNoiseReductionAvailable } from '@/features/rooms/livekit/hooks/useNoiseReductionAvailable'
type RowWrapperProps = {
heading: string
children: ReactNode[]
beta?: boolean
}
const BetaBadge = () => (
<span
className={css({
content: '"Beta"',
display: 'block',
letterSpacing: '-0.02rem',
padding: '0 0.25rem',
backgroundColor: '#E8EDFF',
color: '#0063CB',
fontSize: '12px',
fontWeight: 500,
margin: '0 0 0.9375rem 0.3125rem',
lineHeight: '1rem',
borderRadius: '4px',
width: 'fit-content',
height: 'fit-content',
marginTop: { base: '10px', sm: '5px' },
})}
>
Beta
</span>
)
const RowWrapper = ({ heading, children, beta }: RowWrapperProps) => {
const RowWrapper = ({ heading, children }: RowWrapperProps) => {
return (
<>
<HStack>
<H lvl={2}>{heading}</H>
{beta && <BetaBadge />}
</HStack>
<H lvl={2}>{heading}</H>
<HStack
gap={0}
style={{
@@ -91,12 +60,6 @@ export const AudioTab = ({ id }: AudioTabProps) => {
const { t } = useTranslation('settings')
const { localParticipant } = useRoomContext()
const {
userChoices: { noiseReductionEnabled },
saveAudioInputDeviceId,
saveNoiseReductionEnabled,
} = usePersistentUserChoices()
const isSpeaking = useIsSpeaking(localParticipant)
const {
@@ -143,8 +106,6 @@ export const AudioTab = ({ id }: AudioTabProps) => {
return defaultItem.value
}
const noiseReductionAvailable = useNoiseReductionAvailable()
return (
<TabPanel padding={'md'} flex id={id}>
<RowWrapper heading={t('audio.microphone.heading')}>
@@ -155,10 +116,7 @@ export const AudioTab = ({ id }: AudioTabProps) => {
defaultSelectedKey={
activeDeviceIdIn || getDefaultSelectedKey(itemsIn)
}
onSelectionChange={(key) => {
setActiveMediaDeviceIn(key as string)
saveAudioInputDeviceId(key as string)
}}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
{...disabledProps}
style={{
width: '100%',
@@ -168,7 +126,7 @@ export const AudioTab = ({ id }: AudioTabProps) => {
{localParticipant.isMicrophoneEnabled ? (
<ActiveSpeaker isSpeaking={isSpeaking} />
) : (
<span>{t('audio.microphone.disabled')}</span>
<span>Micro désactivé</span>
)}
</>
</RowWrapper>
@@ -194,23 +152,6 @@ export const AudioTab = ({ id }: AudioTabProps) => {
<SoundTester />
</RowWrapper>
)}
{noiseReductionAvailable && (
<RowWrapper heading={t('audio.noiseReduction.heading')} beta>
<Switch
aria-label={t(
`audio.noiseReduction.ariaLabel.${noiseReductionEnabled ? 'disable' : 'enable'}`
)}
isSelected={noiseReductionEnabled}
onChange={(v) => {
saveNoiseReductionEnabled(v)
if (v) posthog.capture('noise-reduction-init')
}}
>
{t('audio.noiseReduction.label')}
</Switch>
<div />
</RowWrapper>
)}
</TabPanel>
)
}
@@ -29,15 +29,5 @@
"screenRecording": {
"started": "{{name}} hat die Aufzeichnung des Treffens gestartet.",
"stopped": "{{name}} hat die Aufzeichnung des Treffens gestoppt."
},
"recordingSave": {
"transcript": {
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald die Transkription fertig ist.",
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald die Transkription fertig ist."
},
"screenRecording": {
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald sie fertig ist.",
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald sie fertig ist."
}
}
}
+2 -12
View File
@@ -223,11 +223,6 @@
"loading": "Transkription wird gestartet",
"linkMore": "Mehr erfahren"
},
"notAdminOrOwner": {
"heading": "Zugriff eingeschränkt",
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Transkription (Beta) starten.",
"linkMore": "Mehr erfahren"
},
"stop": {
"heading": "Transkription läuft...",
"body": "Die Transkription deines Meetings läuft. Du erhältst das Ergebnis per E-Mail, sobald das Meeting beendet ist.",
@@ -235,7 +230,7 @@
},
"stopping": {
"heading": "Daten werden gespeichert…",
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
"body": "Dieser Vorgang kann einige Minuten dauern. Danke für deine Geduld."
},
"beta": {
"heading": "Werde Beta-Tester",
@@ -259,14 +254,9 @@
"loading": "Aufzeichnung wird gestartet",
"linkMore": "Mehr erfahren"
},
"notAdminOrOwner": {
"heading": "Zugriff eingeschränkt",
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine videoaufnahme (Beta) starten.",
"linkMore": "Mehr erfahren"
},
"stopping": {
"heading": "Daten werden gespeichert…",
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
"body": "Dieser Vorgang kann einige Minuten dauern. Danke für deine Geduld."
},
"stop": {
"heading": "Aufzeichnung läuft…",
+1 -10
View File
@@ -9,16 +9,7 @@
"audio": {
"microphone": {
"heading": "Mikrofon",
"label": "Wählen Sie Ihre Audioeingabe",
"disabled": "Mikrofon deaktiviert"
},
"noiseReduction": {
"label": "Rauschunterdrückung",
"heading": "Rauschunterdrückung",
"ariaLabel": {
"enable": "Rauschunterdrückung aktivieren",
"disable": "Rauschunterdrückung deaktivieren"
}
"label": "Wählen Sie Ihre Audioeingabe"
},
"speakers": {
"heading": "Lautsprecher",
@@ -29,15 +29,5 @@
"screenRecording": {
"started": "{{name}} started the meeting recording.",
"stopped": "{{name}} stopped the meeting recording."
},
"recordingSave": {
"transcript": {
"message": "Your recording is being saved! Well send the transcript to <strong>{{email}}</strong> as soon as its ready.",
"default": "Your recording is being saved! Well send the transcript to your email as soon as its ready."
},
"screenRecording": {
"message": "Your recording is being saved! Well send a notification to <strong>{{email}}</strong> as soon as its ready.",
"default": "Your recording is being saved! Well send a notification to your email as soon as its ready."
}
}
}
+2 -12
View File
@@ -223,11 +223,6 @@
"loading": "Transcription starting",
"linkMore": "Learn more"
},
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a transcription (beta).",
"linkMore": "Learn more"
},
"stop": {
"heading": "Transcription in progress...",
"body": "The transcription of your meeting is in progress. You will receive the result by email once the meeting is finished.",
@@ -235,7 +230,7 @@
},
"stopping": {
"heading": "Saving your data…",
"body": "You can leave the meeting if you wish; the recording will finish automatically."
"body": "This process may take a few minutes. Thank you for your patience."
},
"beta": {
"heading": "Become a beta tester",
@@ -259,14 +254,9 @@
"loading": "Recording starting",
"linkMore": "Learn more"
},
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a recording (beta).",
"linkMore": "Learn more"
},
"stopping": {
"heading": "Saving your data…",
"body": "You can leave the meeting if you wish; the recording will finish automatically."
"body": "This process may take a few minutes. Thank you for your patience."
},
"stop": {
"heading": "Recording in progress…",
+1 -10
View File
@@ -9,16 +9,7 @@
"audio": {
"microphone": {
"heading": "Microphone",
"label": "Select your audio input",
"disabled": "Microphone disabled"
},
"noiseReduction": {
"label": "Noise reduction",
"heading": "Noise reduction",
"ariaLabel": {
"enable": "Enable noise reduction",
"disable": "Disable noise reduction"
}
"label": "Select your audio input"
},
"speakers": {
"heading": "Speakers",
@@ -29,15 +29,5 @@
"screenRecording": {
"started": "{{name}} a démarré l'enregistrement de la réunion.",
"stopped": "{{name}} a arrêté l'enregistrement de la réunion."
},
"recordingSave": {
"transcript": {
"message": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail à <strong>{{email}}</strong> dès que la transcription sera prête.",
"default": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail dès que la transcription sera prête."
},
"screenRecording": {
"message": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail à <strong>{{email}}</strong> dès quil sera prêt.",
"default": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail dès quil sera prêt."
}
}
}
+5 -15
View File
@@ -20,7 +20,7 @@
},
"heading": "Rejoindre la réunion ?",
"effects": {
"description": "Effets d'arrière plan",
"description": "Appliquer des effets",
"title": "Effets",
"subTitle": "Paramétrez les effets de votre caméra."
},
@@ -128,7 +128,7 @@
"feedback": "Partager votre avis",
"settings": "Paramètres",
"username": "Choisir votre nom",
"effects": "Effets d'arrière plan",
"effects": "Appliquer des effets",
"switchCamera": "Changer de caméra",
"fullscreen": {
"enter": "Plein écran",
@@ -223,11 +223,6 @@
"loading": "Démarrage de la transcription",
"linkMore": "En savoir plus"
},
"notAdminOrOwner": {
"heading": "Accès restreint",
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer une transcription (beta).",
"linkMore": "En savoir plus"
},
"stop": {
"heading": "Transcription en cours …",
"body": "La transcription de votre réunion est en cours. Vous recevrez le resultat par email une fois la réunion terminée.",
@@ -235,7 +230,7 @@
},
"stopping": {
"heading": "Sauvegarde de vos données…",
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
"body": "Cette opération peut durer quelques minutes. Merci de votre patience."
},
"beta": {
"heading": "Devenez beta testeur",
@@ -259,14 +254,9 @@
"loading": "Démarrage de l'enregistrement",
"linkMore": "En savoir plus"
},
"notAdminOrOwner": {
"heading": "Accès restreint",
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer un enregistrement (beta).",
"linkMore": "En savoir plus"
},
"stopping": {
"heading": "Sauvegarde de vos données…",
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
"body": "Cette opération peut durer quelques minutes. Merci de votre patience."
},
"stop": {
"heading": "Enregistrement en cours …",
@@ -381,7 +371,7 @@
"enable": "Épingler",
"disable": "Annuler l'épinglage"
},
"effects": "Effets d'arrière plan",
"effects": "Appliquer des effets",
"muteParticipant": "Couper le micro de {{name}}",
"fullScreen": "Plein écran"
},
+1 -10
View File
@@ -9,16 +9,7 @@
"audio": {
"microphone": {
"heading": "Micro",
"label": "Sélectionner votre entrée audio",
"disabled": "Micro désactivé"
},
"noiseReduction": {
"label": "Réduction de bruit",
"heading": "Réduction de bruit",
"ariaLabel": {
"enable": "Activer la réduction du bruit",
"disable": "Désactiver la réduction du bruit"
}
"label": "Sélectionner votre entrée audio"
},
"speakers": {
"heading": "Haut-parleurs",
@@ -29,15 +29,5 @@
"screenRecording": {
"started": "{{name}} is begonnen met het opnemen van de vergadering.",
"stopped": "{{name}} is gestopt met het opnemen van de vergadering."
},
"recordingSave": {
"transcript": {
"message": "We zijn uw opname aan het voltooien! U ontvangt een e-mail op <strong>{{email}}</strong> zodra de transcriptie klaar is.",
"default": "We zijn uw opname aan het voltooien! U ontvangt een e-mail zodra de transcriptie klaar is."
},
"screenRecording": {
"message": "We zijn uw opname aan het voltooien! U ontvangt een e-mail op <strong>{{email}}</strong> zodra deze klaar is.",
"default": "We zijn uw opname aan het voltooien! U ontvangt een e-mail zodra deze klaar is."
}
}
}
+2 -12
View File
@@ -223,11 +223,6 @@
"loading": "Transcriptie begint",
"linkMore": "Meer informatie"
},
"notAdminOrOwner": {
"heading": "Toegang beperkt",
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een transcriptie starten (beta).",
"linkMore": "Meer informatie"
},
"stop": {
"heading": "Transcriptie bezig...",
"body": "De transcriptie van uw vergadering is bezig. U ontvangt het resultaat per e-mail zodra de vergadering is afgelopen.",
@@ -235,7 +230,7 @@
},
"stopping": {
"heading": "Uw gegevens worden opgeslagen…",
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
"body": "Dit proces kan enkele minuten duren. Bedankt voor uw geduld."
},
"beta": {
"heading": "Word betatester",
@@ -259,14 +254,9 @@
"loading": "Opname gestarten",
"linkMore": "Meer informatie"
},
"notAdminOrOwner": {
"heading": "Toegang beperkt",
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een opname starten (beta).",
"linkMore": "Meer informatie"
},
"stopping": {
"heading": "Uw gegevens worden opgeslagen…",
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
"body": "Dit proces kan enkele minuten duren. Bedankt voor uw geduld."
},
"stop": {
"heading": "Opname bezig …",
+1 -10
View File
@@ -9,16 +9,7 @@
"audio": {
"microphone": {
"heading": "Microfoon",
"label": "Selecteer uw audioinvoer",
"disabled": "Microfoon uitgeschakeld"
},
"noiseReduction": {
"label": "Ruisonderdrukking",
"heading": "Ruisonderdrukking",
"ariaLabel": {
"enable": "Ruisonderdrukking inschakelen",
"disable": "Ruisonderdrukking uitschakelen"
}
"label": "Selecteer uw audioinvoer"
},
"speakers": {
"heading": "Luidsprekers",
-25
View File
@@ -1,25 +0,0 @@
import { proxy, subscribe } from 'valtio'
import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur'
import {
loadUserChoices,
saveUserChoices,
LocalUserChoices as LocalUserChoicesLK,
} from '@livekit/components-core'
export type LocalUserChoices = LocalUserChoicesLK & {
processorSerialized?: ProcessorSerialized
noiseReductionEnabled?: boolean
}
function getUserChoicesState(): LocalUserChoices {
return {
noiseReductionEnabled: false,
...loadUserChoices(),
}
}
export const userChoicesStore = proxy<LocalUserChoices>(getUserChoicesState())
subscribe(userChoicesStore, () => {
saveUserChoices(userChoicesStore, false)
})
@@ -67,8 +67,7 @@ backend:
SUMMARY_SERVICE_ENDPOINT: http://meet-summary:80/api/v1/tasks/
SUMMARY_SERVICE_API_TOKEN: password
SCREEN_RECORDING_BASE_URL: https://meet.127.0.0.1.nip.io/recordings
ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
migrate:
@@ -76,13 +75,6 @@ backend:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
@@ -99,20 +91,13 @@ backend:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
@@ -163,7 +148,7 @@ summary:
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: large-v2
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
WEBHOOK_API_TOKEN: password
@@ -196,7 +181,7 @@ celery:
AWS_S3_SECRET_ACCESS_KEY: password
OPENAI_API_KEY: password
OPENAI_BASE_URL: https://albertine.beta.numerique.gouv.fr/v1
OPENAI_ASR_MODEL: large-v2
OPENAI_ASR_MODEL: openai/whisper-large-v3
OPENAI_LLM_MODEL: meta-llama/Llama-3.1-8B-Instruct
AWS_S3_SECURE_ACCESS: False
WEBHOOK_API_TOKEN: password
+2 -17
View File
@@ -94,8 +94,7 @@ backend:
name: backend
key: BREVO_API_KEY
BREVO_API_CONTACT_LIST_IDS: 8
ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
migrate:
@@ -103,13 +102,6 @@ backend:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
@@ -126,20 +118,13 @@ backend:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py createsuperuser --email admin@example.com --password admin
restartPolicy: Never
# Extra volume mounts to manage our local custom CA and avoid to set ssl_verify: false
extraVolumeMounts:
- name: certs
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
mountPath: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.9
version: 0.0.6
+92 -95
View File
@@ -4,104 +4,101 @@
### General configuration
| Name | Description | Value |
| ---------------------------------------------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------ |
| `image.repository` | Repository to use to pull meet's container image | `lasuite/meet-backend` |
| `image.tag` | meet's container tag | `latest` |
| `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
| `image.credentials.username` | Username for container registry authentication | |
| `image.credentials.password` | Password for container registry authentication | |
| `image.credentials.registry` | Registry url for which the credentials are specified | |
| `image.credentials.name` | Name of the generated secret for imagePullSecrets | |
| `nameOverride` | Override the chart name | `""` |
| `fullnameOverride` | Override the full application name | `""` |
| `ingress.enabled` | whether to enable the Ingress or not | `false` |
| `ingress.className` | IngressClass to use for the Ingress | `nil` |
| `ingress.host` | Host for the Ingress | `meet.example.com` |
| `ingress.path` | Path to use for the Ingress | `/` |
| `ingress.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingress.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingress.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingress.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingress.customBackends` | Add custom backends to ingress | `[]` |
| `ingressAdmin.enabled` | whether to enable the Ingress or not | `false` |
| `ingressAdmin.className` | IngressClass to use for the Ingress | `nil` |
| `ingressAdmin.host` | Host for the Ingress | `meet.example.com` |
| `ingressAdmin.path` | Path to use for the Ingress | `/admin` |
| `ingressAdmin.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingressAdmin.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingressMedia.enabled` | whether to enable the Ingress or not | `false` |
| `ingressMedia.className` | IngressClass to use for the Ingress | `nil` |
| `ingressMedia.host` | Host for the Ingress | `meet.example.com` |
| `ingressMedia.path` | Path to use for the Ingress | `/media/(.*)` |
| `ingressMedia.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingressMedia.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingressMedia.tls.secretName` | Secret name for TLS config | `nil` |
| `ingressMedia.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingressMedia.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url` | | `https://meet.example.com/api/v1.0/recordings/media-auth/` |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers` | | `Authorization, X-Amz-Date, X-Amz-Content-SHA256` |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost` | | `minio.meet.svc.cluster.local:9000` |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/configuration-snippet` | | `add_header Content-Security-Policy "default-src 'none'" always;
` |
| `serviceMedia.host` | | `minio.meet.svc.cluster.local` |
| `serviceMedia.port` | | `9000` |
| `serviceMedia.annotations` | | `{}` |
| Name | Description | Value |
| ---------------------------------------------------------------------------- | ---------------------------------------------------- | ----------------------------------------------------------------- |
| `image.repository` | Repository to use to pull meet's container image | `lasuite/meet-backend` |
| `image.tag` | meet's container tag | `latest` |
| `image.pullPolicy` | Container image pull policy | `IfNotPresent` |
| `image.credentials.username` | Username for container registry authentication | |
| `image.credentials.password` | Password for container registry authentication | |
| `image.credentials.registry` | Registry url for which the credentials are specified | |
| `image.credentials.name` | Name of the generated secret for imagePullSecrets | |
| `nameOverride` | Override the chart name | `""` |
| `fullnameOverride` | Override the full application name | `""` |
| `ingress.enabled` | whether to enable the Ingress or not | `false` |
| `ingress.className` | IngressClass to use for the Ingress | `nil` |
| `ingress.host` | Host for the Ingress | `meet.example.com` |
| `ingress.path` | Path to use for the Ingress | `/` |
| `ingress.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingress.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingress.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingress.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingress.customBackends` | Add custom backends to ingress | `[]` |
| `ingressAdmin.enabled` | whether to enable the Ingress or not | `false` |
| `ingressAdmin.className` | IngressClass to use for the Ingress | `nil` |
| `ingressAdmin.host` | Host for the Ingress | `meet.example.com` |
| `ingressAdmin.path` | Path to use for the Ingress | `/admin` |
| `ingressAdmin.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingressAdmin.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingressAdmin.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingressAdmin.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingressMedia.enabled` | whether to enable the Ingress or not | `false` |
| `ingressMedia.className` | IngressClass to use for the Ingress | `nil` |
| `ingressMedia.host` | Host for the Ingress | `meet.example.com` |
| `ingressMedia.path` | Path to use for the Ingress | `/media/(.*)` |
| `ingressMedia.hosts` | Additional host to configure for the Ingress | `[]` |
| `ingressMedia.tls.enabled` | Weather to enable TLS for the Ingress | `true` |
| `ingressMedia.tls.secretName` | Secret name for TLS config | `nil` |
| `ingressMedia.tls.additional[].secretName` | Secret name for additional TLS config | |
| `ingressMedia.tls.additional[].hosts[]` | Hosts for additional TLS config | |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-url` | | `https://meet.example.com/api/v1.0/recordings/media-auth/` |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/auth-response-headers` | | `Authorization, X-Amz-Date, X-Amz-Content-SHA256` |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/upstream-vhost` | | `minio.meet.svc.cluster.local:9000` |
| `ingressMedia.annotations.nginx.ingress.kubernetes.io/configuration-snippet` | | `add_header Content-Security-Policy "default-src 'none'" always;` |
| `serviceMedia.host` | | `minio.meet.svc.cluster.local` |
| `serviceMedia.port` | | `9000` |
| `serviceMedia.annotations` | | `{}` |
### backend
| Name | Description | Value |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `backend.dpAnnotations` | Annotations to add to the backend Deployment | `{}` |
| `backend.command` | Override the backend container command | `[]` |
| `backend.args` | Override the backend container args | `[]` |
| `backend.replicas` | Amount of backend replicas | `3` |
| `backend.shareProcessNamespace` | Enable share process namespace between containers | `false` |
| `backend.sidecars` | Add sidecars containers to backend deployment | `[]` |
| `backend.migrateJobAnnotations` | Annotations for the migrate job | `{}` |
| `backend.jobs.ttlSecondsAfterFinished` | Period to wait before remove jobs | `30` |
| `backend.jobs.backoffLimit` | Numbers of jobs retries | `2` |
| `backend.securityContext` | Configure backend Pod security context | `nil` |
| `backend.envVars` | Configure backend container environment variables | `undefined` |
| `backend.envVars.BY_VALUE` | Example environment variable by setting value directly | |
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.name` | Name of a ConfigMap when configuring env vars from a ConfigMap | |
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
| `backend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
| `backend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
| `backend.podAnnotations` | Annotations to add to the backend Pod | `{}` |
| `backend.service.type` | backend Service type | `ClusterIP` |
| `backend.service.port` | backend Service listening port | `80` |
| `backend.service.targetPort` | backend container listening port | `8000` |
| `backend.service.annotations` | Annotations to add to the backend Service | `{}` |
| `backend.migrate.command` | backend migrate command | `["/bin/sh","-c","while ! python manage.py check --database default > /dev/null 2>&1\ndo\n echo \"Database not ready\"\n sleep 2\ndone\necho \"Database is ready\"\n\npython manage.py migrate --no-input\n"]` |
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.createsuperuser.command` | backend migrate command | `["/bin/sh","-c","while ! python manage.py check --database default > /dev/null 2>&1\ndo\n echo \"Database not ready\"\n sleep 2\ndone\necho \"Database is ready\"\n\npython manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD\n"]` |
| `backend.createsuperuser.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `30` |
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `30` |
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `30` |
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `30` |
| `backend.resources` | Resource requirements for the backend container | `{}` |
| `backend.nodeSelector` | Node selector for the backend Pod | `{}` |
| `backend.tolerations` | Tolerations for the backend Pod | `[]` |
| `backend.affinity` | Affinity for the backend Pod | `{}` |
| `backend.persistence` | Additional volumes to create and mount on the backend. Used for debugging purposes | `{}` |
| `backend.persistence.volume-name.size` | Size of the additional volume | |
| `backend.persistence.volume-name.type` | Type of the additional volume, persistentVolumeClaim or emptyDir | |
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
| Name | Description | Value |
| ----------------------------------------------------- | ---------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `backend.dpAnnotations` | Annotations to add to the backend Deployment | `{}` |
| `backend.command` | Override the backend container command | `[]` |
| `backend.args` | Override the backend container args | `[]` |
| `backend.replicas` | Amount of backend replicas | `3` |
| `backend.shareProcessNamespace` | Enable share process namespace between containers | `false` |
| `backend.sidecars` | Add sidecars containers to backend deployment | `[]` |
| `backend.migrateJobAnnotations` | Annotations for the migrate job | `{}` |
| `backend.securityContext` | Configure backend Pod security context | `nil` |
| `backend.envVars` | Configure backend container environment variables | `undefined` |
| `backend.envVars.BY_VALUE` | Example environment variable by setting value directly | |
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.name` | Name of a ConfigMap when configuring env vars from a ConfigMap | |
| `backend.envVars.FROM_CONFIGMAP.configMapKeyRef.key` | Key within a ConfigMap when configuring env vars from a ConfigMap | |
| `backend.envVars.FROM_SECRET.secretKeyRef.name` | Name of a Secret when configuring env vars from a Secret | |
| `backend.envVars.FROM_SECRET.secretKeyRef.key` | Key within a Secret when configuring env vars from a Secret | |
| `backend.podAnnotations` | Annotations to add to the backend Pod | `{}` |
| `backend.service.type` | backend Service type | `ClusterIP` |
| `backend.service.port` | backend Service listening port | `80` |
| `backend.service.targetPort` | backend container listening port | `8000` |
| `backend.service.annotations` | Annotations to add to the backend Service | `{}` |
| `backend.migrate.command` | backend migrate command | `["python","manage.py","migrate","--no-input"]` |
| `backend.migrate.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.createsuperuser.command` | backend migrate command | `["/bin/sh","-c","python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD\n"]` |
| `backend.createsuperuser.restartPolicy` | backend migrate job restart policy | `Never` |
| `backend.probes.liveness.path` | Configure path for backend HTTP liveness probe | `/__heartbeat__` |
| `backend.probes.liveness.targetPort` | Configure port for backend HTTP liveness probe | `undefined` |
| `backend.probes.liveness.initialDelaySeconds` | Configure initial delay for backend liveness probe | `30` |
| `backend.probes.liveness.initialDelaySeconds` | Configure timeout for backend liveness probe | `30` |
| `backend.probes.startup.path` | Configure path for backend HTTP startup probe | `undefined` |
| `backend.probes.startup.targetPort` | Configure port for backend HTTP startup probe | `undefined` |
| `backend.probes.startup.initialDelaySeconds` | Configure initial delay for backend startup probe | `undefined` |
| `backend.probes.startup.initialDelaySeconds` | Configure timeout for backend startup probe | `undefined` |
| `backend.probes.readiness.path` | Configure path for backend HTTP readiness probe | `/__lbheartbeat__` |
| `backend.probes.readiness.targetPort` | Configure port for backend HTTP readiness probe | `undefined` |
| `backend.probes.readiness.initialDelaySeconds` | Configure initial delay for backend readiness probe | `30` |
| `backend.probes.readiness.initialDelaySeconds` | Configure timeout for backend readiness probe | `30` |
| `backend.resources` | Resource requirements for the backend container | `{}` |
| `backend.nodeSelector` | Node selector for the backend Pod | `{}` |
| `backend.tolerations` | Tolerations for the backend Pod | `[]` |
| `backend.affinity` | Affinity for the backend Pod | `{}` |
| `backend.persistence` | Additional volumes to create and mount on the backend. Used for debugging purposes | `{}` |
| `backend.persistence.volume-name.size` | Size of the additional volume | |
| `backend.persistence.volume-name.type` | Type of the additional volume, persistentVolumeClaim or emptyDir | |
| `backend.persistence.volume-name.mountPath` | Path where the volume should be mounted to | |
| `backend.extraVolumeMounts` | Additional volumes to mount on the backend. | `[]` |
| `backend.extraVolumes` | Additional volumes to mount on the backend. | `[]` |
| `backend.pdb.enabled` | Enable pdb on backend | `true` |
### frontend
@@ -14,8 +14,6 @@ metadata:
labels:
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
spec:
ttlSecondsAfterFinished: {{ .Values.backend.jobs.ttlSecondsAfterFinished }}
backoffLimit: {{ .Values.backend.jobs.backoffLimit }}
template:
metadata:
annotations:
@@ -14,8 +14,6 @@ metadata:
labels:
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
spec:
ttlSecondsAfterFinished: {{ .Values.backend.jobs.ttlSecondsAfterFinished }}
backoffLimit: {{ .Values.backend.jobs.backoffLimit }}
template:
metadata:
annotations:
+2 -2
View File
@@ -60,7 +60,7 @@ spec:
serviceName: {{ include "meet.frontend.fullname" . }}
servicePort: {{ .Values.frontend.service.port }}
{{- end }}
- path: /api/
- path: /api
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
@@ -96,7 +96,7 @@ spec:
serviceName: {{ include "meet.frontend.fullname" $ }}
servicePort: {{ $.Values.frontend.service.port }}
{{- end }}
- path: /api/
- path: /api
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
+4 -24
View File
@@ -136,12 +136,6 @@ backend:
## @param backend.migrateJobAnnotations Annotations for the migrate job
migrateJobAnnotations: {}
## @param backend.jobs.ttlSecondsAfterFinished Period to wait before remove jobs
## @param backend.jobs.backoffLimit Numbers of jobs retries
jobs:
ttlSecondsAfterFinished: 30
backoffLimit: 2
## @param backend.securityContext Configure backend Pod security context
securityContext: null
@@ -172,17 +166,10 @@ backend:
## @param backend.migrate.restartPolicy backend migrate job restart policy
migrate:
command:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py migrate --no-input
- "python"
- "manage.py"
- "migrate"
- "--no-input"
restartPolicy: Never
## @param backend.createsuperuser.command backend migrate command
@@ -192,13 +179,6 @@ backend:
- "/bin/sh"
- "-c"
- |
while ! python manage.py check --database default > /dev/null 2>&1
do
echo "Database not ready"
sleep 2
done
echo "Database is ready"
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
restartPolicy: Never
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.26",
"version": "0.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.26",
"version": "0.1.22",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.26",
"version": "0.1.22",
"description": "An util to generate html and text django's templates from mjml templates",
"type": "module",
"dependencies": {
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sdk",
"version": "0.1.26",
"version": "0.1.22",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.26",
"version": "0.1.22",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.26",
"version": "0.1.22",
"author": "",
"license": "ISC",
"description": "",
+2 -4
View File
@@ -1,6 +1,4 @@
FROM python:3.13-slim AS base
FROM base AS builder
FROM python:3.12-slim AS builder
WORKDIR /app
@@ -8,7 +6,7 @@ COPY pyproject.toml .
RUN pip3 install --no-cache-dir .
FROM base AS production
FROM python:3.12-slim AS production
WORKDIR /app
+6 -6
View File
@@ -1,23 +1,23 @@
[project]
name = "summary"
version = "0.1.26"
version = "0.1.22"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"celery==5.5.3",
"celery==5.4.0",
"redis==5.2.1",
"minio==7.2.15",
"openai==1.91.0",
"requests==2.32.4",
"sentry-sdk[fastapi, celery]==2.30.0",
"openai==1.68.2",
"requests==2.32.3",
"sentry-sdk[fastapi, celery]==2.24.1",
]
[project.optional-dependencies]
dev = [
"ruff==0.12.0",
"ruff==0.11.2",
]
[build-system]
+8 -33
View File
@@ -36,28 +36,6 @@ if settings.sentry_dsn and settings.sentry_is_enabled:
sentry_sdk.init(dsn=settings.sentry_dsn, enable_tracing=True)
DEFAULT_EMPTY_TRANSCRIPTION = """
**Aucun contenu audio na été détecté dans votre transcription.**
*Si vous pensez quil sagit dune erreur, nhésitez pas à contacter
notre support technique : visio@numerique.gouv.fr*
.
.
.
Quelques points que nous vous conseillons de vérifier :
- Un micro était-il activé ?
- Étiez-vous suffisamment proche ?
- Le micro est-il de bonne qualité ?
- Lenregistrement dure-t-il plus de 30 secondes ?
"""
def save_audio_stream(audio_stream, chunk_size=32 * 1024):
"""Save an audio stream to a temporary OGG file."""
with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
@@ -86,9 +64,9 @@ def format_segments(transcription_data):
conversation with speaker labels.
"""
formatted_output = ""
if not transcription_data or not hasattr(transcription_data, "segments"):
if isinstance(transcription_data, dict) and "segments" in transcription_data:
segments = transcription_data["segments"]
if not transcription_data or not hasattr(transcription_data, 'segments'):
if isinstance(transcription_data, dict) and 'segments' in transcription_data:
segments = transcription_data['segments']
else:
return "Error: Invalid transcription data format"
else:
@@ -97,8 +75,8 @@ def format_segments(transcription_data):
previous_speaker = None
for segment in segments:
speaker = segment.get("speaker", "UNKNOWN_SPEAKER")
text = segment.get("text", "")
speaker = segment.get('speaker', 'UNKNOWN_SPEAKER')
text = segment.get('text', '')
if text:
if speaker != previous_speaker:
formatted_output += f"\n\n **{speaker}**: {text}"
@@ -107,7 +85,6 @@ def format_segments(transcription_data):
previous_speaker = speaker
return formatted_output
def post_with_retries(url, data):
"""Send POST request with automatic retries."""
session = create_retry_session()
@@ -241,11 +218,7 @@ def process_audio_transcribe_summarize_v2(filename: str, email: str, sub: str):
os.remove(temp_file_path)
logger.debug("Temporary file removed: %s", temp_file_path)
formatted_transcription = (
DEFAULT_EMPTY_TRANSCRIPTION
if not transcription.segments
else format_segments(transcription)
)
formatted_transcription = format_segments(transcription)
data = {
"title": "Transcription",
@@ -263,3 +236,5 @@ def process_audio_transcribe_summarize_v2(filename: str, email: str, sub: str):
logger.debug("Response body: %s", response.text)
# TODO - integrate summarize the transcript and create a new document.