Compare commits

..

34 Commits

Author SHA1 Message Date
lebaudantoine d1bb414cb4 🔖(minor) bump release to 0.1.26
Add noise reduction with RNNoise.
New feature needs to be battle tested,
it's protected with a feature flag.
2025-06-25 22:40:01 +02:00
lebaudantoine 513f3c588a 🚩(frontend) add noise reduction availability hook with feature flag
Encapsulate noise reduction availability logic in hook and add feature flag
for quick production disable if issues arise. Gives product owners emergency
control over the feature.
2025-06-25 22:21:23 +02:00
lebaudantoine b407bfda07 🐛(frontend) default noise reduction to disabled for existing users
Prevent React warnings about uncontrolled/controlled components by ensuring
lk-user-choice store initializes with default value when noise reduction
setting is missing from existing localStorage.
2025-06-25 22:21:23 +02:00
lebaudantoine 3de4cc01dc (frontend) disable noise reduction on mobile devices
Prevent enabling noise reduction on mobile to avoid performance issues
from resource-intensive audio processing.
2025-06-25 22:21:23 +02:00
lebaudantoine 1fe1a557ad (frontend) add hook to manage noise reduction processor toggling
Create concise hook that listens to audio track status and user noise
reduction preference to automatically handle processor state changes.

Note: Logging could be improved in future iterations.
2025-06-25 22:21:23 +02:00
lebaudantoine fb92e5b79f (frontend) add noise reduction toggle setting with beta label
Implement settings option to enable/disable noise suppression with clear
beta indicator. Label will be removed after production battle-testing.

Note: Settings styling needs polish, will address in future commits.
2025-06-25 22:21:23 +02:00
lebaudantoine e71bc093bd (frontend) add RNNoise processor for meeting noise suppression
Implement noise reduction copying Jitsi's approach. RNNoise isn't optimal
but chosen for first draft. Needs production battle-testing for CPU/RAM.

Use global audio context with pause/resume instead of deletion to avoid
WASM resource leak issues in @timephy/rnnoise-wasm dependency. Audio context
deletion may not properly release WASM resources.

Requires discussion with senior devs on resource management approach.
2025-06-25 22:21:23 +02:00
lebaudantoine 43df855461 (frontend) add @timephy/rnnoise-wasm for noise suppression
Install wrapper around Jitsi's RNNoise implementation for easier reuse.
Note: Library may not properly release WebAssembly resources based on
code review.
2025-06-25 22:21:23 +02:00
lebaudantoine 12fc33d30a 🔖(minor) bump release to 0.1.25
Fix chat issue livekit/components-js#1173
Bump backend dependencies
Switch to python v13
2025-06-25 19:59:34 +02:00
lebaudantoine 892a98193d 🎨(backend) format sources and clean up ruff configuration
Apply formatting changes from recent ruff upgrade and remove obsolete
ignored error rules that are no longer needed.
2025-06-25 15:02:44 +02:00
lebaudantoine c5379f29e7 (backend) remove unused june-analytics-python dependency
Clean up dependencies by removing unused analytics package to reduce
bundle size and eliminate unnecessary maintenance overhead.
2025-06-25 15:02:44 +02:00
lebaudantoine 2fddc82333 🔧(backend) fix Redis dependency conflict by downgrading to v5.2.1
Resolve ResolutionImpossible error where meet requires redis==6.2.0
but kombu[redis] 5.5.x requires redis<=5.2.1. Downgrade to redis==5.2.1
satisfies overlapping constraints and testing confirms compatibility.

Pin the redis dependency.
2025-06-25 15:02:44 +02:00
renovate[bot] c9ba6cbc05 ⬆️(dependencies) update python dependencies 2025-06-25 15:02:44 +02:00
lebaudantoine 76807a54f2 🚑️(frontend) bump livekit-react-components to fix critical chat issue
Urgent update to resolve chat functionality problems.

Refs: livekit/components-js#1173, #1174
2025-06-25 14:31:43 +02:00
lebaudantoine 15aff4db8e 🌐(frontend) add missing internationalization for "micro désactivé"
Fix overlooked French text that wasn't properly internationalized in
the microphone status display.
2025-06-24 20:09:02 +02:00
lebaudantoine 27a0128b2a 🐛(frontend) fix audio device ID persistence from settings panel
Ensure user's audio device selection is properly saved to localStorage
when changed through settings to maintain preference across sessions.
2025-06-24 20:09:02 +02:00
lebaudantoine 4a18e188e4 ♻️(frontend) refactor usePersistentUserChoice to fix state sync issues
I may have introduced a misusage of the usePersistentUserChoice hook.
I ended using it while expecting it to be a global state, it wasn't.

Fix broken global state that caused user choice desync. Use LiveKit default
persistence functions similar to notification store approach. Carefully
handles existing localStorage data to prevent regressions.

Note: Audio output persistence will be added in future commits.
2025-06-24 20:09:02 +02:00
lebaudantoine 1cd8fd2fc6 🔒️(backend) enhance participant ID serialization in lobby per audit
Improve participant ID handling in lobby serialization following security
auditor recommendations to prevent potential data exposure.
2025-06-24 13:57:53 +02:00
lebaudantoine 64eadadaef 🔒️(backend) clarify administrator role checking function names
Rename vague functions to explicitly indicate administrator permission checks,
or owner ones. Prevents developer confusion and potential security misuse
per auditor recommendations.
2025-06-24 13:57:53 +02:00
lebaudantoine 6e48f8f222 🔒️(backend) remove realistic password data from test fixtures
Replace test fixture passwords that resembled real credentials to avoid
confusion during security audits and follow security best practices.
2025-06-24 13:57:53 +02:00
lebaudantoine 866a2cea20 🔒️(backend) specify explicit imports to limit security exposure
Replace wildcard imports with specific function imports, particularly for
OS package which could expose dangerous functions. Follows security audit
recommendations to minimize attack surface.
2025-06-24 13:57:53 +02:00
lebaudantoine 17b1dde050 ♻️(summary) dry docker image summary by extracting base image name
Eliminate duplication by using variables for base image references to
improve maintainability and reduce configuration redundancy.
2025-06-23 16:36:02 +02:00
lebaudantoine 0b25374cef ⬆️(docker) upgrade backend image to python 3.13
Python 3.13 is now stable, our libraries are compatible with it. We also
upgrade the alpine version and node one used in the backend.
2025-06-23 16:36:02 +02:00
lebaudantoine fb8b2d752b 🔒️(backend) upgrade Django to 5.2.3 for security compliance
Update Django and related libraries per security auditor recommendations
as current version is aging. Django 5.2.3+ changed email validation per

Remove failing test cases affected by stricter validation.

Refs:
- https://code.djangoproject.com/ticket/36014
- https://github.com/django/django/commit/c068f000
2025-06-23 14:59:01 +02:00
lebaudantoine 3c0a712f45 🔖(minor) bump release to 0.1.24
Fix blurry screenshare issue.
See livekit/client-sdk-js#1556
2025-06-22 19:54:01 +02:00
lebaudantoine 17795c69d6 🩹(frontend) add temp workaround for LiveKit useChat breaking changes
Implement brittle message count tracking to handle chat emissions after
useChat API changes. Temporary fix until refactoring to new text stream
approach recommended by LiveKit team.

Ref: https://github.com/livekit/components-js/issues/1158
2025-06-20 23:20:47 +02:00
lebaudantoine d1b0378a45 ⬆️(frontend) bump livekit-client from 2.11 to 2.13
Fix critical Chrome screenshare bug and other issues. See:
https://github.com/livekit/client-sdk-js/pull/1556

Breaking change: Chat functionality affected by behavior changes.
Discussion: https://github.com/livekit/components-js/issues/1158
2025-06-20 23:20:47 +02:00
lebaudantoine ef4dcf57b0 📈(frontend) add analytics tracking for connection events
Implement connection event tracking to monitor user connectivity
patterns and identify potential issues.
2025-06-12 14:54:51 +02:00
lebaudantoine bbb6e4f317 ♻️(frontend) refactor useConnectionObserver to track session time
Add session duration tracking and consolidate all disconnect events
(including client-initiated) with timing data for comprehensive
connection analytics.
2025-06-12 14:54:51 +02:00
lebaudantoine 77d2365a61 🐛(backend) fix OIDC returnTo parameter validation in Docker compose
Correct OIDC_REDIRECT_ALLOWED_HOSTS configuration that was preventing proper
URL validation. Thanks to @nathanvss for identifying and fixing the issue.

Note: Update your common env file with corrected values.
2025-06-11 18:34:41 +02:00
lebaudantoine e6caa0a2fd 📈(frontend) add analytics for disconnect/reconnect events
Track connection issues to identify user problems. Skip client-initiated
disconnects (normal flow). Disconnect events provide richer data than
reconnect events which lack reason details.

Next: Add error screen for JOIN_FAILURE disconnects to trigger support
workflow for users experiencing connection problems.
2025-06-11 18:28:29 +02:00
lebaudantoine f9614fc108 🩹(backend) default CORS_ALLOW_ALL_ORIGINS to False
The settings CORS_ALLOW_ALL_ORIGINS was set to True by default.

This error is inherited from a old mistake made back in the days
while working on the initial impress demo.

I wrongly configured the settings. This error was propagated when
@sampaccoud copied impress code to kickstart LaSuite Meet.

This is not something we want, this should be only allowed in
development. We change the value in all the manifests in order to have
the desired behavior in non development environments.
2025-06-10 16:16:55 +02:00
Jacques ROUSSEL 9d516bf638 🚸(helm) improve helm chart
Our Helm chart wasn't suitable for use with Helm alone because jobs
remained after deployment. We chose to configure ttlSecondsAfterFinished
to clean up jobs after a period of time.
2025-06-06 16:52:30 +02:00
lebaudantoine 5bac9a1d59 🔒️(frontend) hide Nginx server version in error responses
Remove version disclosure in /assets/ error pages identified by security
auditor to prevent information leakage vulnerability.
2025-06-03 15:17:21 +02:00
67 changed files with 848 additions and 492 deletions
+2 -2
View File
@@ -81,7 +81,7 @@ jobs:
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: "3.13"
- 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.12"
python-version: "3.13"
- name: Install development dependencies
run: pip install --user .[dev]
+1 -1
View File
@@ -1,7 +1,7 @@
# Django Meet
# ---- base image to inherit from ----
FROM python:3.12.6-alpine3.20 AS base
FROM python:3.13.5-alpine3.21 AS base
# Upgrade pip to its latest release to speed up dependencies installation
RUN python -m pip install --upgrade pip setuptools
+5 -5
View File
@@ -250,7 +250,7 @@ You can use Visio on https://meet.127.0.0.1.nip.io from the local device. The pr
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 | |
@@ -270,7 +270,7 @@ These are the environmental options available on meet backend.
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
| DJANGO_LANGUAGE_CODE | Default language | en-us |
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
| 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 |
@@ -293,7 +293,7 @@ These are the environmental options available on meet backend.
| 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 | true |
| 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 | |
@@ -351,10 +351,10 @@ These are the environmental options available on meet backend.
| 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_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_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 |
+1 -1
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=["http://localhost:8083", "http://localhost:3000"]
OIDC_REDIRECT_ALLOWED_HOSTS=localhost:8083,localhost:3000
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
# Livekit Token settings
+1 -1
View File
@@ -7,7 +7,7 @@
"enabled": false,
"groupName": "ignored python dependencies",
"matchManagers": ["pep621"],
"matchPackageNames": []
"matchPackageNames": ["redis"]
},
{
"enabled": false,
-1
View File
@@ -42,7 +42,6 @@ def get_frontend_configuration(request):
"available_modes": settings.RECORDING_WORKER_CLASSES.keys(),
"expiration_days": settings.RECORDING_EXPIRATION_DAYS,
},
"custom_css": "/custom.css",
}
frontend_configuration.update(settings.FRONTEND_CONFIGURATION)
return Response(frontend_configuration)
+3 -3
View File
@@ -64,7 +64,7 @@ class RoomPermissions(permissions.BasePermission):
if request.method == "DELETE":
return obj.is_owner(user)
return obj.is_administrator(user)
return obj.is_administrator_or_owner(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(user)
return obj.resource.is_administrator_or_owner(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_owner(request.user) or obj.is_administrator(request.user)
return obj.is_administrator_or_owner(request.user)
class IsRecordingEnabled(permissions.BasePermission):
+19 -5
View File
@@ -1,5 +1,7 @@
"""Client serializers for the Meet core app."""
import uuid
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
@@ -58,7 +60,9 @@ class ResourceAccessSerializerMixin:
request = self.context.get("request", None)
user = getattr(request, "user", None)
if not (user and user.is_authenticated and resource.is_administrator(user)):
if not (
user and user.is_authenticated and resource.is_administrator_or_owner(user)
):
raise PermissionDenied(
_("You must be administrator or owner of a room to add accesses to it.")
)
@@ -118,9 +122,11 @@ class RoomSerializer(serializers.ModelSerializer):
return output
role = instance.get_role(request.user)
is_admin = models.RoleChoices.check_administrator_role(role)
is_admin_or_owner = models.RoleChoices.check_administrator_role(
role
) or models.RoleChoices.check_owner_role(role)
if is_admin:
if is_admin_or_owner:
access_serializer = NestedResourceAccessSerializer(
instance.accesses.select_related("resource", "user").all(),
context=self.context,
@@ -128,7 +134,7 @@ class RoomSerializer(serializers.ModelSerializer):
)
output["accesses"] = access_serializer.data
if not is_admin:
if not is_admin_or_owner:
del output["configuration"]
should_access_room = (
@@ -147,7 +153,7 @@ class RoomSerializer(serializers.ModelSerializer):
room_id=room_id, user=request.user, username=username
)
output["is_administrable"] = is_admin
output["is_administrable"] = is_admin_or_owner
return output
@@ -215,6 +221,14 @@ 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,4 +1,3 @@
# ruff: noqa: S311
"""
Core application factories
"""
+7 -7
View File
@@ -35,7 +35,7 @@ class RoleChoices(models.TextChoices):
@classmethod
def check_administrator_role(cls, role):
"""Check if a role is administrator."""
return role in [cls.ADMIN, cls.OWNER]
return role == cls.ADMIN
@classmethod
def check_owner_role(cls, role):
@@ -288,13 +288,13 @@ class Resource(BaseModel):
role = RoleChoices.MEMBER
return role
def is_administrator(self, user):
def is_administrator_or_owner(self, user):
"""
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))
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)
def is_owner(self, user):
"""Check if a user is owner of the resource."""
@@ -287,9 +287,7 @@ 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}_participant1",
f"mocked-cache-prefix_{room.id}_2f7f162fe7d1421b90e702bfbfbf8def",
{
"id": "participant1",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"username": "user1",
"status": "waiting",
"color": "#123456",
},
)
cache.set(
f"mocked-cache-prefix_{room.id}_participant2",
f"mocked-cache-prefix_{room.id}_f4ca3ab8a6c04ad88097b8da33f60f10",
{
"id": "participant2",
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
"username": "user2",
"status": "accepted",
"color": "#654321",
@@ -257,7 +257,9 @@ 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="123"
LobbyService,
"_get_or_create_participant_id",
return_value="2f7f162fe7d1421b90e702bfbfbf8def",
),
mock.patch.object(
utils, "generate_livekit_config", return_value={"token": "test-token"}
@@ -274,11 +276,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 == "123"
assert cookie.value == "2f7f162fe7d1421b90e702bfbfbf8def"
# Verify response content matches expected structure and values
assert response.json() == {
"id": "123",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"username": "test_user",
"status": "accepted",
"color": "mocked-color",
@@ -300,9 +302,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}_participant1",
f"mocked-cache-prefix_{room.id}_2f7f162fe7d1421b90e702bfbfbf8def",
{
"id": "participant1",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"username": "user1",
"status": "waiting",
"color": "#123456",
@@ -310,7 +312,7 @@ def test_request_entry_waiting_participant_public_room(settings):
)
# Simulate a browser with existing participant cookie
client.cookies.load({"mocked-cookie": "participant1"})
client.cookies.load({"mocked-cookie": "2f7f162fe7d1421b90e702bfbfbf8def"})
with (
mock.patch.object(LobbyService, "notify_participants", return_value=None),
@@ -328,11 +330,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 == "participant1"
assert cookie.value == "2f7f162fe7d1421b90e702bfbfbf8def"
# Verify response content matches expected structure and values
assert response.json() == {
"id": "participant1",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"username": "user1",
"status": "accepted",
"color": "#123456",
@@ -379,7 +381,7 @@ def test_allow_participant_to_enter_anonymous():
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "test-id", "allow_entry": True},
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
)
assert response.status_code == 401
@@ -394,7 +396,7 @@ def test_allow_participant_to_enter_non_owner():
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "test-id", "allow_entry": True},
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
)
assert response.status_code == 403
@@ -412,7 +414,7 @@ def test_allow_participant_to_enter_public_room():
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "test-id", "allow_entry": True},
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
)
assert response.status_code == 404
@@ -435,9 +437,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}_participant1",
f"mocked-cache-prefix_{room.id!s}_2f7f162fe7d1421b90e702bfbfbf8def",
{
"id": "test-id",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"status": "waiting",
"username": "foo",
"color": "123",
@@ -446,13 +448,18 @@ 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": "participant1", "allow_entry": allow_entry},
{
"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def",
"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}_participant1")
participant_data = cache.get(
f"mocked-cache-prefix_{room.id!s}_2f7f162fe7d1421b90e702bfbfbf8def"
)
assert participant_data.get("status") == updated_status
@@ -468,12 +475,14 @@ 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}_test-id")
participant_data = cache.get(
f"mocked-cache-prefix_{room.id!s}_2f7f162fe7d1421b90e702bfbfbf8def"
)
assert participant_data is None
response = client.post(
f"/api/v1.0/rooms/{room.id}/enter/",
{"participant_id": "test-id", "allow_entry": True},
{"participant_id": "2f7f162fe7d1421b90e702bfbfbf8def", "allow_entry": True},
)
assert response.status_code == 404
@@ -563,18 +572,18 @@ def test_list_waiting_participants_success(settings):
# Add participants in the lobby
cache.set(
f"mocked-cache-prefix_{room.id}_participant1",
f"mocked-cache-prefix_{room.id}_2f7f162fe7d1421b90e702bfbfbf8def",
{
"id": "participant1",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"username": "user1",
"status": "waiting",
"color": "#123456",
},
)
cache.set(
f"mocked-cache-prefix_{room.id}_participant2",
f"mocked-cache-prefix_{room.id}_f4ca3ab8a6c04ad88097b8da33f60f10",
{
"id": "participant2",
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
"username": "user2",
"status": "waiting",
"color": "#654321",
@@ -588,13 +597,13 @@ def test_list_waiting_participants_success(settings):
participants = response.json().get("participants")
assert sorted(participants, key=lambda p: p["id"]) == [
{
"id": "participant1",
"id": "2f7f162fe7d1421b90e702bfbfbf8def",
"username": "user1",
"status": "waiting",
"color": "#123456",
},
{
"id": "participant2",
"id": "f4ca3ab8a6c04ad88097b8da33f60f10",
"username": "user2",
"status": "waiting",
"color": "#654321",
@@ -2,7 +2,6 @@
Test LiveKit webhook endpoint on the rooms API.
"""
# ruff: noqa: PLR0913
# pylint: disable=R0913,W0621,R0917,W0613
import base64
import hashlib
@@ -25,7 +24,7 @@ def webhook_event_data():
"name": "00000000-0000-0000-0000-000000000000",
"emptyTimeout": 300,
"creationTime": "1692627281",
"turnPassword": "2Pvdj+/WV1xV4EkB8klJ9xkXDWY=",
"turnPassword": "fake-turn-password",
"enabledCodecs": [
{"mime": "audio/opus"},
{"mime": "video/H264"},
+6 -6
View File
@@ -102,7 +102,7 @@ def test_models_rooms_access_rights_none(django_assert_num_queries):
with django_assert_num_queries(0):
assert room.get_role(None) is None
with django_assert_num_queries(0):
assert room.is_administrator(None) is False
assert room.is_administrator_or_owner(None) is False
with django_assert_num_queries(0):
assert room.is_owner(None) is False
@@ -115,7 +115,7 @@ def test_models_rooms_access_rights_anonymous(django_assert_num_queries):
with django_assert_num_queries(0):
assert room.get_role(user) is None
with django_assert_num_queries(0):
assert room.is_administrator(user) is False
assert room.is_administrator_or_owner(user) is False
with django_assert_num_queries(0):
assert room.is_owner(user) is False
@@ -128,7 +128,7 @@ def test_models_rooms_access_rights_authenticated(django_assert_num_queries):
with django_assert_num_queries(1):
assert room.get_role(user) is None
with django_assert_num_queries(1):
assert room.is_administrator(user) is False
assert room.is_administrator_or_owner(user) is False
with django_assert_num_queries(1):
assert room.is_owner(user) is False
@@ -141,7 +141,7 @@ def test_models_rooms_access_rights_member_direct(django_assert_num_queries):
with django_assert_num_queries(1):
assert room.get_role(user) == "member"
with django_assert_num_queries(1):
assert room.is_administrator(user) is False
assert room.is_administrator_or_owner(user) is False
with django_assert_num_queries(1):
assert room.is_owner(user) is False
@@ -154,7 +154,7 @@ def test_models_rooms_access_rights_administrator_direct(django_assert_num_queri
with django_assert_num_queries(1):
assert room.get_role(user) == "administrator"
with django_assert_num_queries(1):
assert room.is_administrator(user) is True
assert room.is_administrator_or_owner(user) is True
with django_assert_num_queries(1):
assert room.is_owner(user) is False
@@ -167,7 +167,7 @@ def test_models_rooms_access_rights_owner_direct(django_assert_num_queries):
with django_assert_num_queries(1):
assert room.get_role(user) == "owner"
with django_assert_num_queries(1):
assert room.is_administrator(user) is True
assert room.is_administrator_or_owner(user) is True
with django_assert_num_queries(1):
assert room.is_owner(user) is True
+3 -3
View File
@@ -3,12 +3,12 @@
meet's sandbox management script.
"""
import os
import sys
from os import environ
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
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."""
import os
from os import environ
from celery import Celery
from configurations.importer import install
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
environ.setdefault("DJANGO_CONFIGURATION", "Development")
install(check_options=True)
+9 -12
View File
@@ -11,7 +11,7 @@ https://docs.djangoproject.com/en/3.1/ref/settings/
"""
import json
import os
from os import path
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 = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BASE_DIR = path.dirname(path.dirname(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(os.path.join(BASE_DIR, "version.json"), encoding="utf8") as version:
with open(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(os.path.join("/", "data"), environ_name="DATA_DIR")
DATA_DIR = values.Value(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 = os.path.join(DATA_DIR, "static")
STATIC_ROOT = path.join(DATA_DIR, "static")
MEDIA_URL = "/media/"
MEDIA_ROOT = os.path.join(DATA_DIR, "media")
MEDIA_ROOT = path.join(DATA_DIR, "media")
SITE_ID = 1
@@ -166,7 +166,7 @@ class Base(Configuration):
)
)
LOCALE_PATHS = (os.path.join(BASE_DIR, "locale"),)
LOCALE_PATHS = (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": [os.path.join(BASE_DIR, "templates")],
"DIRS": [path.join(BASE_DIR, "templates")],
"OPTIONS": {
"context_processors": [
"django.contrib.auth.context_processors.auth",
@@ -319,9 +319,6 @@ class Base(Configuration):
"feedback": values.DictValue(
{}, environ_name="FRONTEND_FEEDBACK", environ_prefix=None
),
"transcript": values.DictValue(
{}, environ_name="FRONTEND_TRANSCRIPT", environ_prefix=None
)
}
# Mail
@@ -343,7 +340,7 @@ class Base(Configuration):
# CORS
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(True)
CORS_ALLOW_ALL_ORIGINS = values.BooleanValue(False)
CORS_ALLOWED_ORIGINS = values.ListValue([])
CORS_ALLOWED_ORIGIN_REGEXES = values.ListValue([])
+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/
"""
import os
from os import environ
from configurations.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
os.environ.setdefault("DJANGO_CONFIGURATION", "Development")
environ.setdefault("DJANGO_SETTINGS_MODULE", "meet.settings")
environ.setdefault("DJANGO_CONFIGURATION", "Development")
application = get_wsgi_application()
+27 -28
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.23"
version = "0.1.26"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
@@ -25,39 +25,38 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.37.24",
"boto3==1.38.42",
"Brotli==1.1.0",
"brevo-python==1.1.2",
"celery[redis]==5.4.0",
"celery[redis]==5.5.3",
"django-configurations==2.5.1",
"django-cors-headers==4.7.0",
"django-countries==7.6.1",
"django-lasuite==0.0.7",
"django-lasuite==0.0.10",
"django-parler==2.3",
"redis==5.2.1",
"django-redis==5.4.0",
"django-storages[s3]==1.14.5",
"django-redis==6.0.0",
"django-storages[s3]==1.14.6",
"django-timezone-field>=5.1",
"django==5.1.9",
"djangorestframework==3.15.2",
"django==5.2.3",
"djangorestframework==3.16.0",
"drf_spectacular==0.28.0",
"dockerflow==2024.4.2",
"easy_thumbnails==2.10",
"factory_boy==3.3.3",
"gunicorn==23.0.0",
"jsonschema==4.23.0",
"june-analytics-python==2.3.0",
"markdown==3.7",
"jsonschema==4.24.0",
"markdown==3.8.2",
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.2.6",
"psycopg[binary]==3.2.9",
"PyJWT==2.10.1",
"python-frontmatter==1.1.0",
"requests==2.32.3",
"sentry-sdk==2.24.1",
"requests==2.32.4",
"sentry-sdk==2.30.0",
"whitenoise==6.9.0",
"mozilla-django-oidc==4.0.1",
"livekit-api==1.0.2",
"aiohttp==3.11.14",
"livekit-api==1.0.3",
"aiohttp==3.12.13",
]
[project.urls]
@@ -68,22 +67,22 @@ dependencies = [
[project.optional-dependencies]
dev = [
"django-extensions==3.2.3",
"drf-spectacular-sidecar==2025.3.1",
"freezegun==1.5.1",
"django-extensions==4.1",
"drf-spectacular-sidecar==2025.6.1",
"freezegun==1.5.2",
"ipdb==0.13.13",
"ipython==9.0.2",
"pyfakefs==5.8.0",
"ipython==9.3.0",
"pyfakefs==5.9.1",
"pylint-django==2.6.1",
"pylint==3.3.6",
"pytest-cov==6.0.0",
"pytest-django==4.10.0",
"pytest==8.3.5",
"pylint==3.3.7",
"pytest-cov==6.2.1",
"pytest-django==4.11.1",
"pytest==8.4.1",
"pytest-icdiff==0.9",
"pytest-xdist==3.6.1",
"pytest-xdist==3.7.0",
"responses==0.25.7",
"ruff==0.11.2",
"types-requests==2.32.0.20250306",
"ruff==0.12.0",
"types-requests==2.32.4.20250611",
]
[tool.setuptools]
+1
View File
@@ -1,6 +1,7 @@
server {
listen 8080;
server_name localhost;
server_tokens off;
root /usr/share/nginx/html;
+79 -55
View File
@@ -1,27 +1,28 @@
{
"name": "meet",
"version": "0.1.23",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.23",
"version": "0.1.26",
"dependencies": {
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.5",
"@livekit/track-processors": "0.5.6",
"@livekit/components-react": "2.9.12",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.5.7",
"@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.11.4",
"livekit-client": "2.13.8",
"posthog-js": "1.240.6",
"react": "18.3.1",
"react-aria-components": "1.8.0",
@@ -405,9 +406,9 @@
}
},
"node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
"integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==",
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.1.tgz",
"integrity": "sha512-wJ8ReQbHxsAfXhrf9ixl0aYbZorRuOWpBNzm8pL8ftmSxQx/wnJD5Eg861NwJU/czy2VXFIebCeZnZrI9rktIQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@clack/core": {
@@ -981,22 +982,22 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz",
"integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.1.tgz",
"integrity": "sha512-azI0DrjMMfIug/ExbBaeDVJXcY0a7EPvPjb2xAJPa4HeimBX+Z18HK8QQR3jb6356SnDDdxx+hinMLcJEDdOjw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.8"
"@floating-ui/utils": "^0.2.9"
}
},
"node_modules/@floating-ui/utils": {
@@ -1214,48 +1215,39 @@
}
},
"node_modules/@livekit/components-core": {
"version": "0.12.1",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.1.tgz",
"integrity": "sha512-R7qWoVzPckOYxEHZgP3Kp8u+amu+isnTptgoZV7+bpmLRBHI7mWnaD+0uDWlyIMjI1pBbK3wHg0ILKa5UytI+A==",
"version": "0.12.8",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.8.tgz",
"integrity": "sha512-ZqQ88DkZZw6h4XY/lFklOFsM76zZX0mIpa6HKxDgMgW3QpDjl7oOpQCHZYvaDhmJJ9X2m58oOCuf3RUdTKSJMA==",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/dom": "1.6.11",
"@floating-ui/dom": "1.6.13",
"loglevel": "1.9.1",
"rxjs": "7.8.1"
"rxjs": "7.8.2"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"livekit-client": "^2.8.1",
"livekit-client": "^2.13.3",
"tslib": "^2.6.2"
}
},
"node_modules/@livekit/components-core/node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/@livekit/components-react": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.8.1.tgz",
"integrity": "sha512-XpuDu7iDMcN4pkV8CYNzHf9hLNdYOeEtbmCr7Zesy6Au3BxUl4aS1Ajmg0b75Rx7zTlkyCJt9Lm4VrEqbJCI6Q==",
"version": "2.9.12",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.12.tgz",
"integrity": "sha512-GSbVNEeJSGvjyRzUVHJvBahAvrC/zAG7gOD+UlgYnxjA1fEte4gSUtwbcdVauABGWZGtiaU2cQvSuNhCQaXRZQ==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/components-core": "0.12.1",
"@livekit/components-core": "0.12.8",
"clsx": "2.1.1",
"usehooks-ts": "3.1.0"
"usehooks-ts": "3.1.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@livekit/krisp-noise-filter": "^0.2.12",
"livekit-client": "^2.8.1",
"livekit-client": "^2.13.3",
"react": ">=18",
"react-dom": ">=18",
"tslib": "^2.6.2"
@@ -1267,9 +1259,9 @@
}
},
"node_modules/@livekit/components-styles": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@livekit/components-styles/-/components-styles-1.1.5.tgz",
"integrity": "sha512-SocIPcwm18S28zVruvJcmiHfbUIwGTfxGbUOIp1Db78EON/iJ2v7B2g/xD5sr+c7jXoV1DNUPl2qQiFW2S9dbw==",
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@livekit/components-styles/-/components-styles-1.1.6.tgz",
"integrity": "sha512-V6zfuREC2ksW8z6T6WSbEvdLB5ICVikGz1GtLr59UcxHDyAsKDbuDHAyl3bF3xBqPKYmY3GWF3Qk39rnScyOtA==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
@@ -1282,23 +1274,24 @@
"license": "Apache-2.0"
},
"node_modules/@livekit/protocol": {
"version": "1.36.1",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.36.1.tgz",
"integrity": "sha512-nN3QnITAQ5yXk7UKfotH7CRWIlEozNWeKVyFJ0/+dtSzvWP/ib+10l1DDnRYi3A1yICJOGAKFgJ5d6kmi1HCUA==",
"version": "1.39.2",
"resolved": "https://registry.npmjs.org/@livekit/protocol/-/protocol-1.39.2.tgz",
"integrity": "sha512-kYbIO/JlC6cylSxd4WJrBps9+zoZ9gifL7t3iW9whT8rbo5jHx03I4dwBLhzOonVyX+memSEO90m/ymNoT+aAw==",
"license": "Apache-2.0",
"dependencies": {
"@bufbuild/protobuf": "^1.10.0"
}
},
"node_modules/@livekit/track-processors": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.5.6.tgz",
"integrity": "sha512-TlzObrSlp2PKor4VXqg6iefLRFVEb2T1lXwddBFdkPod60XVgYoMOj7V5xJm+UTE2MEtlE0003vUli9PyQGB1g==",
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@livekit/track-processors/-/track-processors-0.5.7.tgz",
"integrity": "sha512-/2SkuVAF+YiPNtOi9zQJz/yH1WGaK53XZ3PaESpLOiEYUBsYky13BrriXCXUf6kwn5R5+7ZsYWc2k3XSsAuLtg==",
"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"
}
},
@@ -3896,6 +3889,11 @@
"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",
@@ -3949,6 +3947,30 @@
"@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",
@@ -7347,13 +7369,13 @@
}
},
"node_modules/livekit-client": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.11.4.tgz",
"integrity": "sha512-V82NdyhHo3iBxxQmPTk95Fe2+VZMABWZL56t9oZmMgycoF8li8gs/KG7yyDsKki50EeKLgwOmlwqx/Xf0pc0+Q==",
"version": "2.13.8",
"resolved": "https://registry.npmjs.org/livekit-client/-/livekit-client-2.13.8.tgz",
"integrity": "sha512-HXE89EizbgS/V+gh74GLGYOzvQ9d8qAsWt/N5lYkVGJX8PCq+7WK8whq5baFF7FBGkVPaE5NmW7Nk2V5nHvvWA==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/mutex": "1.1.1",
"@livekit/protocol": "1.36.1",
"@livekit/protocol": "1.39.2",
"events": "^3.3.0",
"loglevel": "^1.9.2",
"sdp-transform": "^2.15.0",
@@ -7361,6 +7383,9 @@
"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": {
@@ -8672,7 +8697,6 @@
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"tslib": "^2.1.0"
}
@@ -9502,9 +9526,9 @@
}
},
"node_modules/usehooks-ts": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz",
"integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz",
"integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==",
"license": "MIT",
"dependencies": {
"lodash.debounce": "^4.0.8"
@@ -9513,7 +9537,7 @@
"node": ">=16.15.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18"
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/util-deprecate": {
+6 -5
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.23",
"version": "0.1.26",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,20 +13,21 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.5",
"@livekit/track-processors": "0.5.6",
"@livekit/components-react": "2.9.12",
"@livekit/components-styles": "1.1.6",
"@livekit/track-processors": "0.5.7",
"@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.11.4",
"livekit-client": "2.13.8",
"posthog-js": "1.240.6",
"react": "18.3.1",
"react-aria-components": "1.8.0",
-7
View File
@@ -10,19 +10,12 @@ export interface ApiConfig {
}
support?: {
id: string
help_article_transcript: string
help_article_recording: string
help_article_more_tools: string
}
feedback: {
url: string
}
transcript: {
form_beta_users: string
}
silence_livekit_debug_logs?: boolean
is_silent_login_enabled?: boolean
custom_css?: string
recording?: {
is_enabled?: boolean
available_modes?: RecordingMode[]
@@ -3,7 +3,6 @@ import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
import { useSyncUserPreferencesWithBackend } from '@/features/auth'
import { useEffect } from 'react'
export const AppInitialization = () => {
const { data } = useConfig()
@@ -13,22 +12,11 @@ export const AppInitialization = () => {
analytics = {},
support = {},
silence_livekit_debug_logs = false,
custom_css = '',
} = data || {}
useAnalytics(analytics)
useSupport(support)
useEffect(() => {
if (custom_css) {
const link = document.createElement('link')
link.href = custom_css
link.id = 'visio-custom-css'
link.rel = 'stylesheet'
document.head.appendChild(link)
}
}, [custom_css])
silenceLiveKitLogs(silence_livekit_debug_logs)
return null
@@ -10,7 +10,8 @@ export const NotFoundScreen = () => {
<Screen layout="centered">
<CenteredContent title={t('notFound.heading')} withBackButton>
<Text centered>
{t('notFound.body')} <Bold>{window.origin}/xxx-yyyy-zzz.</Bold>
{t('notFound.body')}{' '}
<Bold>https://visio.numerique.gouv.fr/xxx-yyyy-zzz.</Bold>
</Text>
</CenteredContent>
</Screen>
@@ -2,4 +2,5 @@ export enum FeatureFlags {
Transcript = 'transcription-summary',
ScreenRecording = 'screen-recording',
faceLandmarks = 'face-landmarks',
noiseReduction = 'noise-reduction',
}
@@ -8,7 +8,7 @@ import { Button, LinkButton } from '@/primitives'
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useConfig } from '@/api/useConfig'
import { BETA_USERS_FORM_URL } from '@/utils/constants'
const Heading = styled('h2', {
base: {
@@ -171,8 +171,6 @@ export const IntroSlider = () => {
const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
const NUMBER_SLIDES = SLIDES.length
const { data } = useConfig()
return (
<Container>
<div
@@ -205,7 +203,7 @@ export const IntroSlider = () => {
<Body>{t(`${slide.key}.body`)}</Body>
{slide.isAvailableInBeta && (
<LinkButton
href={data?.transcript.form_beta_users}
href={BETA_USERS_FORM_URL}
target="_blank"
tooltip={t('beta.tooltip')}
variant={'primary'}
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
name="roomId"
label={t('joinInputLabel')}
description={t('joinInputExample', {
example: window.origin + '/azer-tyu-qsdf',
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
})}
validate={(value) => {
return !isRoomValid(value.trim()) ? (
@@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from 'react'
import { ConnectionState, RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import { CRISP_HELP_ARTICLE_RECORDING } from '@/utils/constants'
import {
NotificationType,
@@ -23,10 +24,8 @@ import {
import posthog from 'posthog-js'
import { useSnapshot } from 'valtio/index'
import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig'
export const ScreenRecordingSidePanel = () => {
const { data } = useConfig()
const [isLoading, setIsLoading] = useState(false)
const recordingSnap = useSnapshot(recordingStore)
const { t } = useTranslation('rooms', { keyPrefix: 'screenRecording' })
@@ -200,11 +199,9 @@ export const ScreenRecordingSidePanel = () => {
})}
>
{t('start.body')} <br />{' '}
{data?.support?.help_article_recording && (
<A href={data.support.help_article_recording} target="_blank">
{t('start.linkMore')}
</A>
)}
<A href={CRISP_HELP_ARTICLE_RECORDING} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
@@ -16,6 +16,10 @@ import { useEffect, useMemo, useState } from 'react'
import { ConnectionState, RoomEvent } from 'livekit-client'
import { useTranslation } from 'react-i18next'
import { RecordingStatus, recordingStore } from '@/stores/recording'
import {
BETA_USERS_FORM_URL,
CRISP_HELP_ARTICLE_TRANSCRIPT,
} from '@/utils/constants'
import { FeatureFlags } from '@/features/analytics/enums'
import {
NotificationType,
@@ -25,11 +29,8 @@ import {
import posthog from 'posthog-js'
import { useSnapshot } from 'valtio/index'
import { Spinner } from '@/primitives/Spinner'
import { useConfig } from '@/api/useConfig'
export const TranscriptSidePanel = () => {
const { data } = useConfig()
const [isLoading, setIsLoading] = useState(false)
const { t } = useTranslation('rooms', { keyPrefix: 'transcript' })
@@ -160,14 +161,9 @@ export const TranscriptSidePanel = () => {
>
{t('notAdminOrOwner.body')}
<br />
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('notAdminOrOwner.linkMore')}
</A>
)}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('notAdminOrOwner.linkMore')}
</A>
</Text>
</>
) : (
@@ -184,19 +180,14 @@ export const TranscriptSidePanel = () => {
})}
>
{t('beta.body')}{' '}
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('start.linkMore')}
</A>
)}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={data?.transcript.form_beta_users}
href={BETA_USERS_FORM_URL}
target="_blank"
>
{t('beta.button')}
@@ -272,14 +263,9 @@ export const TranscriptSidePanel = () => {
})}
>
{t('start.body')} <br />{' '}
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('start.linkMore')}
</A>
)}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<Button
isDisabled={isDisabled}
@@ -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,7 +9,6 @@ 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 {
@@ -28,6 +27,7 @@ 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,40 +116,26 @@ export const Join = ({
const { t } = useTranslation('rooms', { keyPrefix: 'join' })
const {
userChoices: initialUserChoices,
userChoices: {
audioEnabled,
videoEnabled,
audioDeviceId,
videoDeviceId,
processorSerialized,
username,
},
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
saveUsername,
saveProcessorSerialized,
} = usePersistentUserChoices({})
} = usePersistentUserChoices()
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
)
BackgroundProcessorFactory.deserializeProcessor(processorSerialized)
)
useEffect(() => {
saveAudioInputDeviceId(audioDeviceId)
}, [audioDeviceId, saveAudioInputDeviceId])
useEffect(() => {
saveVideoInputDeviceId(videoDeviceId)
}, [videoDeviceId, saveVideoInputDeviceId])
useEffect(() => {
saveUsername(username)
}, [username, saveUsername])
useEffect(() => {
saveProcessorSerialized(processor?.serialize())
}, [
@@ -161,8 +147,8 @@ export const Join = ({
const tracks = usePreviewTracks(
{
audio: { deviceId: initialUserChoices.audioDeviceId },
video: { deviceId: initialUserChoices.videoDeviceId },
audio: { deviceId: audioDeviceId },
video: { deviceId: videoDeviceId },
},
onError
)
@@ -351,9 +337,9 @@ export const Join = ({
</H>
<Field
type="text"
onChange={setUsername}
onChange={saveUsername}
label={t('usernameLabel')}
defaultValue={initialUserChoices?.username}
defaultValue={username}
validate={(value) => !value && t('errors.usernameEmpty')}
wrapperProps={{
noMargin: true,
@@ -474,11 +460,11 @@ export const Join = ({
source={Track.Source.Microphone}
initialState={audioEnabled}
track={audioTrack}
initialDeviceId={initialUserChoices.audioDeviceId}
onChange={(enabled) => setAudioEnabled(enabled)}
initialDeviceId={audioDeviceId}
onChange={(enabled) => saveAudioInputEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
onActiveDeviceChange={(deviceId) =>
setAudioDeviceId(deviceId ?? '')
saveAudioInputDeviceId(deviceId ?? '')
}
variant="tertiary"
/>
@@ -486,11 +472,11 @@ export const Join = ({
source={Track.Source.Camera}
initialState={videoEnabled}
track={videoTrack}
initialDeviceId={initialUserChoices.videoDeviceId}
onChange={(enabled) => setVideoEnabled(enabled)}
initialDeviceId={videoDeviceId}
onChange={(enabled) => saveVideoInputEnabled(enabled)}
onDeviceError={(error) => console.error(error)}
onActiveDeviceChange={(deviceId) =>
setVideoDeviceId(deviceId ?? '')
saveVideoInputDeviceId(deviceId ?? '')
}
variant="tertiary"
/>
@@ -2,6 +2,7 @@ import { A, Div, Text } from '@/primitives'
import { css } from '@/styled-system/css'
import { Button as RACButton } from 'react-aria-components'
import { useTranslation } from 'react-i18next'
import { CRISP_HELP_ARTICLE_MORE_TOOLS } from '@/utils/constants'
import { ReactNode } from 'react'
import { RiFileTextFill, RiLiveFill } from '@remixicon/react'
import { SubPanelId, useSidePanel } from '../hooks/useSidePanel'
@@ -14,7 +15,6 @@ import {
useIsRecordingActive,
} from '@/features/recording'
import { FeatureFlags } from '@/features/analytics/enums'
import { useConfig } from '@/api/useConfig'
export interface ToolsButtonProps {
icon: ReactNode
@@ -113,7 +113,6 @@ const ToolButton = ({
}
export const Tools = () => {
const { data } = useConfig()
const { openTranscript, openScreenRecording, activeSubPanelId } =
useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
@@ -161,14 +160,10 @@ export const Tools = () => {
margin="md"
>
{t('body')}{' '}
{data?.support?.help_article_more_tools && (
<>
<A href={data?.support?.help_article_more_tools} target="_blank">
{t('moreLink')}
</A>
.
</>
)}
<A href={CRISP_HELP_ARTICLE_MORE_TOOLS} target="_blank">
{t('moreLink')}
</A>
.
</Text>
{isTranscriptEnabled && (
<ToolButton
@@ -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) {
@@ -0,0 +1,61 @@
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
}
}, [])
}
@@ -0,0 +1,32 @@
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])
}
@@ -0,0 +1,13 @@
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,71 +1,34 @@
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'
import { useSnapshot } from 'valtio'
import { userChoicesStore } from '@/stores/userChoices'
import { ProcessorSerialized } from '@/features/rooms/livekit/components/blur'
/**
* 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])
export function usePersistentUserChoices() {
const userChoicesSnap = useSnapshot(userChoicesStore)
return {
userChoices,
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
saveUsername,
saveProcessorSerialized,
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
},
}
}
@@ -4,6 +4,7 @@ import {
formatChatMessageLinks,
useChat,
useParticipants,
useRoomContext,
} from '@livekit/components-react'
import { useTranslation } from 'react-i18next'
import { useSnapshot } from 'valtio'
@@ -12,6 +13,7 @@ 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>,
@@ -27,6 +29,7 @@ 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()
@@ -36,6 +39,7 @@ 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
@@ -43,6 +47,19 @@ 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,16 +47,13 @@ export interface ControlBarProps extends React.HTMLAttributes<HTMLDivElement> {
* ```
* @public
*/
export function ControlBar({
saveUserChoices = true,
onDeviceError,
}: ControlBarProps) {
export function ControlBar({ onDeviceError }: ControlBarProps) {
const {
saveAudioInputEnabled,
saveVideoInputEnabled,
saveAudioInputDeviceId,
saveVideoInputDeviceId,
} = usePersistentUserChoices({ preventSave: !saveUserChoices })
} = usePersistentUserChoices()
const microphoneOnChange = React.useCallback(
(enabled: boolean, isUserInitiated: boolean) =>
@@ -30,6 +30,8 @@ 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',
@@ -74,6 +76,8 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const lastAutoFocusedScreenShareTrack =
React.useRef<TrackReferenceOrPlaceholder | null>(null)
useConnectionObserver()
const tracks = useTracks(
[
{ source: Track.Source.Camera, withPlaceholder: true },
@@ -84,6 +88,8 @@ export function VideoConference({ ...props }: VideoConferenceProps) {
const layoutContext = useCreateLayoutContext()
useNoiseReduction()
const screenShareTracks = tracks
.filter(isTrackReference)
.filter((track) => track.publication.source === Track.Source.ScreenShare)
@@ -0,0 +1,102 @@
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,23 +1,16 @@
import { useEffect, useState } from 'react'
import {
usePersistentUserChoices,
type LocalUserChoices as LocalUserChoicesLK,
} from '@livekit/components-react'
import { usePersistentUserChoices } 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'
export type LocalUserChoices = LocalUserChoicesLK & {
processorSerialized?: ProcessorSerialized
}
import { LocalUserChoices } from '@/stores/userChoices'
export const Room = () => {
const { isLoggedIn } = useUser()
@@ -1,4 +1,4 @@
import { DialogProps, Field, H } from '@/primitives'
import { DialogProps, Field, H, Switch } from '@/primitives'
import { TabPanel, TabPanelProps } from '@/primitives/Tabs'
import {
@@ -11,17 +11,48 @@ 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 RowWrapper = ({ heading, children }: RowWrapperProps) => {
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) => {
return (
<>
<H lvl={2}>{heading}</H>
<HStack>
<H lvl={2}>{heading}</H>
{beta && <BetaBadge />}
</HStack>
<HStack
gap={0}
style={{
@@ -60,6 +91,12 @@ export const AudioTab = ({ id }: AudioTabProps) => {
const { t } = useTranslation('settings')
const { localParticipant } = useRoomContext()
const {
userChoices: { noiseReductionEnabled },
saveAudioInputDeviceId,
saveNoiseReductionEnabled,
} = usePersistentUserChoices()
const isSpeaking = useIsSpeaking(localParticipant)
const {
@@ -106,6 +143,8 @@ export const AudioTab = ({ id }: AudioTabProps) => {
return defaultItem.value
}
const noiseReductionAvailable = useNoiseReductionAvailable()
return (
<TabPanel padding={'md'} flex id={id}>
<RowWrapper heading={t('audio.microphone.heading')}>
@@ -116,7 +155,10 @@ export const AudioTab = ({ id }: AudioTabProps) => {
defaultSelectedKey={
activeDeviceIdIn || getDefaultSelectedKey(itemsIn)
}
onSelectionChange={(key) => setActiveMediaDeviceIn(key as string)}
onSelectionChange={(key) => {
setActiveMediaDeviceIn(key as string)
saveAudioInputDeviceId(key as string)
}}
{...disabledProps}
style={{
width: '100%',
@@ -126,7 +168,7 @@ export const AudioTab = ({ id }: AudioTabProps) => {
{localParticipant.isMicrophoneEnabled ? (
<ActiveSpeaker isSpeaking={isSpeaking} />
) : (
<span>Micro désactivé</span>
<span>{t('audio.microphone.disabled')}</span>
)}
</>
</RowWrapper>
@@ -152,6 +194,23 @@ 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>
)
}
+1 -1
View File
@@ -192,7 +192,7 @@
"disclaimer": "Die Nachrichten sind nur für Teilnehmer zum Zeitpunkt des Sendens sichtbar. Alle Nachrichten werden am Ende des Anrufs gelöscht."
},
"moreTools": {
"body": "Greifen Sie auf weitere Tools in Visio zu, um Ihre Meetings zu verbessern.",
"body": "Greifen Sie auf weitere Tools in Visio zu, um Ihre Meetings zu verbessern,",
"moreLink": "mehr erfahren",
"tools": {
"transcript": {
+10 -1
View File
@@ -9,7 +9,16 @@
"audio": {
"microphone": {
"heading": "Mikrofon",
"label": "Wählen Sie Ihre Audioeingabe"
"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"
}
},
"speakers": {
"heading": "Lautsprecher",
+1 -1
View File
@@ -192,7 +192,7 @@
"disclaimer": "The messages are visible to participants only at the time they are sent. All messages are deleted at the end of the call."
},
"moreTools": {
"body": "Access more tools in Visio to enhance your meetings.",
"body": "Access more tools in Visio to enhance your meetings,",
"moreLink": "learn more",
"tools": {
"transcript": {
+10 -1
View File
@@ -9,7 +9,16 @@
"audio": {
"microphone": {
"heading": "Microphone",
"label": "Select your audio input"
"label": "Select your audio input",
"disabled": "Microphone disabled"
},
"noiseReduction": {
"label": "Noise reduction",
"heading": "Noise reduction",
"ariaLabel": {
"enable": "Enable noise reduction",
"disable": "Disable noise reduction"
}
},
"speakers": {
"heading": "Speakers",
+1 -1
View File
@@ -192,7 +192,7 @@
"disclaimer": "Les messages sont visibles par les participants uniquement au moment de\nleur envoi. Tous les messages sont supprimés à la fin de l'appel."
},
"moreTools": {
"body": "Accèder à d'avantage d'outils dans Visio pour améliorer vos réunions.",
"body": "Accèder à d'avantage d'outils dans Visio pour améliorer vos réunions,",
"moreLink": "en savoir plus",
"tools": {
"transcript": {
+10 -1
View File
@@ -9,7 +9,16 @@
"audio": {
"microphone": {
"heading": "Micro",
"label": "Sélectionner votre entrée audio"
"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"
}
},
"speakers": {
"heading": "Haut-parleurs",
+1 -1
View File
@@ -192,7 +192,7 @@
"disclaimer": "De berichten zijn alleen voor de deelnemers zichtbaar op het moment dat ze worden verzonden. Alle berichten worden verwijderd aan het einde van het gesprek."
},
"moreTools": {
"body": "Toegang tot meer tools in Visio om je vergaderingen te verbeteren.",
"body": "Toegang tot meer tools in Visio om je vergaderingen te verbeteren,",
"moreLink": "lees meer",
"tools": {
"transcript": {
+10 -1
View File
@@ -9,7 +9,16 @@
"audio": {
"microphone": {
"heading": "Microfoon",
"label": "Selecteer uw audioinvoer"
"label": "Selecteer uw audioinvoer",
"disabled": "Microfoon uitgeschakeld"
},
"noiseReduction": {
"label": "Ruisonderdrukking",
"heading": "Ruisonderdrukking",
"ariaLabel": {
"enable": "Ruisonderdrukking inschakelen",
"disable": "Ruisonderdrukking uitschakelen"
}
},
"speakers": {
"heading": "Luidsprekers",
+25
View File
@@ -0,0 +1,25 @@
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)
})
+11
View File
@@ -0,0 +1,11 @@
export const BETA_USERS_FORM_URL =
'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17' as const
export const CRISP_HELP_ARTICLE_MORE_TOOLS =
'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23' as const
export const CRISP_HELP_ARTICLE_TRANSCRIPT =
'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x' as const
export const CRISP_HELP_ARTICLE_RECORDING =
'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0' as const
@@ -54,8 +54,7 @@ backend:
LIVEKIT_API_URL: https://livekit.127.0.0.1.nip.io/
ALLOW_UNREGISTERED_ROOMS: False
FRONTEND_SILENCE_LIVEKIT_DEBUG: False
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041', 'help_article_transcript': 'https://lasuite.crisp.help/fr/article/visio-transcript-1sjq43x', 'help_article_recording': 'https://lasuite.crisp.help/fr/article/visio-enregistrement-wgc8o0', 'help_article_more_tools': 'https://lasuite.crisp.help/fr/article/visio-tools-bvxj23'}"
FRONTEND_TRANSCRIPT: "{'form_beta_users': 'https://grist.numerique.gouv.fr/o/docs/forms/3fFfvJoTBEQ6ZiMi8zsQwX/17'}"
FRONTEND_SUPPORT: "{'id': '58ea6697-8eba-4492-bc59-ad6562585041'}"
FRONTEND_FEEDBACK: "{'url': 'https://grist.numerique.gouv.fr/o/docs/cbMv4G7pLY3Z/USER-RESEARCH-or-LA-SUITE/f/26'}"
AWS_S3_ENDPOINT_URL: http://minio.meet.svc.cluster.local:9000
AWS_S3_ACCESS_KEY_ID: meet
@@ -69,7 +68,7 @@ backend:
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.12/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
migrate:
@@ -77,6 +76,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 migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
@@ -93,13 +99,20 @@ 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.12/site-packages/certifi/cacert.pem
mountPath: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
subPath: cacert.pem
# Extra volumes to manage our local custom CA and avoid to set ssl_verify: false
+16 -2
View File
@@ -95,7 +95,7 @@ backend:
key: BREVO_API_KEY
BREVO_API_CONTACT_LIST_IDS: 8
ROOM_TELEPHONY_ENABLED: True
SSL_CERT_FILE: /usr/local/lib/python3.12/site-packages/certifi/cacert.pem
SSL_CERT_FILE: /usr/local/lib/python3.13/site-packages/certifi/cacert.pem
migrate:
@@ -103,6 +103,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 migrate --no-input &&
python manage.py create_demo --force
restartPolicy: Never
@@ -119,13 +126,20 @@ 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.12/site-packages/certifi/cacert.pem
mountPath: /usr/local/lib/python3.13/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.8
version: 0.0.9
+95 -92
View File
@@ -4,101 +4,104 @@
### 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.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` |
| 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` |
### frontend
@@ -14,6 +14,8 @@ 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,6 +14,8 @@ metadata:
labels:
{{- include "meet.common.labels" (list . $component) | nindent 4 }}
spec:
ttlSecondsAfterFinished: {{ .Values.backend.jobs.ttlSecondsAfterFinished }}
backoffLimit: {{ .Values.backend.jobs.backoffLimit }}
template:
metadata:
annotations:
+24 -4
View File
@@ -136,6 +136,12 @@ 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
@@ -166,10 +172,17 @@ backend:
## @param backend.migrate.restartPolicy backend migrate job restart policy
migrate:
command:
- "python"
- "manage.py"
- "migrate"
- "--no-input"
- "/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
restartPolicy: Never
## @param backend.createsuperuser.command backend migrate command
@@ -179,6 +192,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 $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
restartPolicy: Never
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.23",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.23",
"version": "0.1.26",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.23",
"version": "0.1.26",
"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.23",
"version": "0.1.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.23",
"version": "0.1.26",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.23",
"version": "0.1.26",
"author": "",
"license": "ISC",
"description": "",
+4 -2
View File
@@ -1,4 +1,6 @@
FROM python:3.12-slim AS builder
FROM python:3.13-slim AS base
FROM base AS builder
WORKDIR /app
@@ -6,7 +8,7 @@ COPY pyproject.toml .
RUN pip3 install --no-cache-dir .
FROM python:3.12-slim AS production
FROM base AS production
WORKDIR /app
+6 -6
View File
@@ -1,23 +1,23 @@
[project]
name = "summary"
version = "0.1.23"
version = "0.1.26"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"celery==5.4.0",
"celery==5.5.3",
"redis==5.2.1",
"minio==7.2.15",
"openai==1.68.2",
"requests==2.32.3",
"sentry-sdk[fastapi, celery]==2.24.1",
"openai==1.91.0",
"requests==2.32.4",
"sentry-sdk[fastapi, celery]==2.30.0",
]
[project.optional-dependencies]
dev = [
"ruff==0.11.2",
"ruff==0.12.0",
]
[build-system]