Compare commits

..

32 Commits

Author SHA1 Message Date
lebaudantoine f6c9ca9214 wip extract hardcoded version of visio.numerique.gouv.fr 2025-06-03 15:22:08 +02:00
lebaudantoine c91fac7bdd wip extract hardcoded URL 2025-06-03 15:22:08 +02:00
Emmanuel Pelletier ca98507642 add a way to load a custom css file to ease up UI customization 2025-06-02 16:45:39 +02:00
lebaudantoine 61aa3c79c5 🩹(backend) replace requests exception with urllib3 ones
My bad, I caught the wrong exception, issue is still raising in Sentry.
It fixes commit #2a7d963f
2025-05-28 10:49:03 +02:00
lebaudantoine 0c6cd8223d 🔖(minor) bump release to 0.1.23
Misc updates and fixes.
2025-05-27 21:50:21 +02:00
lebaudantoine b692314646 🐛(backend) handle empty transcription files gracefully
Add handling for when users forget to activate microphones resulting in
empty transcripts. User message not yet internationalized, planned for
next version.

Add friendly instructions to give them hint about the situation.
2025-05-27 21:27:09 +02:00
lebaudantoine e816475981 🚨(summary) lint backend sources
Run ruff format to fix linter warnings.
2025-05-27 19:12:37 +02:00
lebaudantoine 8d1f01645a 🚸(frontend) add notification with recording handling details
Display notification clarifying recording is processing and show which email
will receive completion notification. Reduces user mental load per
@sampaccoud's feedback.
2025-05-27 19:12:37 +02:00
lebaudantoine 29a46a413e 🚸(frontend) clarify transcription/recording access restrictions in UI
Make it explicit that non-owner/admin users cannot enable transcription.
Beta feature behavior that may change in future versions.
2025-05-27 19:12:37 +02:00
lebaudantoine b618b2347f 🚸(frontend) clarify automatic recording save process in UI
Add explicit messaging that recording save is automatic and continues
even after leaving meeting. Reduces user anxiety based on feedback
from Samuel Paccoud.
2025-05-27 19:12:37 +02:00
lebaudantoine 7038f2a85d 🐛(backend) fix KeyError crash when role is undefined in request data
Use dict.get() instead of direct key access to prevent server crashes when
role field is missing. Fix inherited from magnify project codebase.
2025-05-27 16:20:36 +02:00
lebaudantoine b7dfafaf47 🔇(backend) downgrade marketing exceptions from error to warning level
Replace logger.exception with logger.warning to reduce Sentry noise for
unavoidable timeout errors that developers cannot act upon.
2025-05-27 15:15:46 +02:00
lebaudantoine 2a7d963f50 🥅(backend) catch request timeout and Brevo contact addition errors
Handle unhandled exceptions to prevent UX impact. Marketing email operations
are optional and should not disrupt core functionality.

My first implementation was imperfect, raising error in sentry.
2025-05-27 15:15:46 +02:00
lebaudantoine 980117132f (backend) add 'sandbox' configuration class for demo environment
Create dedicated config for sandbox environment used for external demos,
bug bounties, and security auditing purposes.
2025-05-27 15:06:02 +02:00
lebaudantoine 7a8b50b5f0 ♻️(backend) use sentry tags instead of extra scope
To ease filtering issues on sentry, we want to use tags instead of extra
scope. Tags are indexed and searchable, it's not the case with extra
scope. Moreover using set_extra to add additional data is deprecated.

Commit #ebf6d46 on docs.
2025-05-27 15:06:02 +02:00
lebaudantoine 409e403581 🚸(backend) display recording owners directly in admin list view
Show recording owner(s) directly in admin list interface to speed up
troubleshooting. Previously required clicking into each object to identify
owner. Handles multiple owners (rare) by displaying a default message.
2025-05-26 14:45:57 +02:00
lebaudantoine 0c0eed6f59 🚸(backend) enhance recording search UX in admin debugging interface
Improve user experience when searching for recording to streamline admin
troubleshooting workflows.
2025-05-26 14:45:57 +02:00
lebaudantoine 750b7f86b4 🚸(backend) replace user select with autocomplete in admin interface
Convert basic select to autocomplete for adding users to rooms. Improves
admin UX when managing 10k+ users
2025-05-26 14:45:57 +02:00
lebaudantoine 9569c58315 🚸(backend) enhance room search UX in admin debugging interface
Improve user experience when searching for rooms to streamline admin
troubleshooting workflows.
2025-05-26 14:45:57 +02:00
Baptiste Massemin 922a968418 🔧(helm) fix the path prefix of the backend ingress
The current path is `/api/v`, and it doesn't work with `ingress-nginx`.
I'm not sure if other ingress controllers work with this prefix,
but changing it to `/api/` will work for `ingress-nginx`
and likely for others as well.
2025-05-23 19:15:33 +02:00
lebaudantoine 48e6cef763 ⬇️(frontend) rollback LiveKit component library from 2.9.0 to 2.8.1
Downgrade @livekit/components-js due to bug introduced in version 2.9.0.
Issue has been reported to upstream maintainers at:
https://github.com/livekit/components-js/issues/1158

Reverting to last known stable version until fix is available.
2025-05-23 19:05:51 +02:00
lebaudantoine ac4ec6c752 📝(backend) update documentation on backend options
Add 19 missing env variables and correct typo in description. Used
Claude to generate the list, please feel free to correct any of these
values/descriptions through PR.
2025-05-23 15:24:01 +02:00
lebaudantoine 7454d44329 🔥(ci) remove unused Crowdin i18n steps
We're not using Crowdin yet and failing CI steps confuse external
contributors. Clean up pipeline to remove unnecessary complexity.
2025-05-23 14:37:16 +02:00
Jacques ROUSSEL 1e3e7de753 🔒️(front) improve docker image security
Cyberwatch reported security issues with the frontend Docker image.
2025-05-23 14:25:06 +02:00
lebaudantoine 4c2eb31a6a ✏️(backend) fix typo in FRONTEND_IS_SILENT_LOGIN_ENABLED env var
Correct spelling in environment variable name as identified by @K900. This
is technically a breaking change for existing deployments using this setting,
but acceptable as we haven't released an official version yet. Will personally
notify known users of this setting about the change to minimize disruption.
2025-05-20 22:54:51 +02:00
lebaudantoine a2a44a6546 🌐(frontend) rename "appliquer des effets" to "effets d'arrière plan"
Update French translation of effects feature based on user feedback to better
reflect its purpose as background effects rather than generic effects.
2025-05-20 22:54:34 +02:00
lebaudantoine f1fa99f918 (backend) allow setting session cookie age via env var
We want to be able to increase the duration of the cookie session
by setting an environment variable.
2025-05-20 13:56:22 +02:00
lebaudantoine 4a6e65d4be 🐛(frontend) restore user language synchronization between front and back
Fix issue where user language preferences stopped properly syncing between
frontend and backend, causing inconsistent language experience. Issue was
reported by user and affected localization settings persistence.
2025-05-20 13:49:54 +02:00
lebaudantoine c8772bb1ad 🐛(backend) update pin code tests after increasing max retry limit
Fix test cases for room PIN code generation that were not updated when
max retry limit was increased during code review. Aligns test expectations
with actual implementation to prevent false failures.
2025-05-19 11:13:59 +02:00
lebaudantoine d54925bd97 🔧(helm) update ASR model name after switch to WhisperX
Correct Automatic Speech Recognition model naming configuration to reflect
the transition from insanely-fast-whisper to WhisperX implementation.
2025-05-19 11:13:59 +02:00
lebaudantoine e9f3e27058 🌐(backend) improve French translations for technical terminology
Refine French translations to be less literal and preserve English terms for
standard technical concepts. Enhances clarity and maintains industry
terminology conventions.
2025-05-19 11:13:59 +02:00
lebaudantoine 0abbe4a26f 🌐(backend) add missing translations for room PIN functionality
Add internationalization support for previously untranslated strings related
to room PIN code logic. Ensures consistent localization across all user-
facing room access features.
2025-05-19 11:13:59 +02:00
57 changed files with 649 additions and 291 deletions
-64
View File
@@ -249,67 +249,3 @@ jobs:
- name: Build SDK
run: npm run build
i18n-crowdin:
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: "infrastructure,secrets"
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ steps.app-token.outputs.token }}
- name: Load sops secrets
uses: rouja/actions-sops@main
with:
secret-file: secrets/numerique-gouv/meet/secrets.enc.env
age-key: ${{ secrets.SOPS_PRIVATE }}
- name: Install gettext (required to make messages)
run: |
sudo apt-get update
sudo apt-get install -y gettext
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install development dependencies
working-directory: src/backend
run: pip install --user .[dev]
- name: Generate the translation base file
run: ~/.local/bin/django-admin makemessages --keep-pot --all
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "18.x"
cache: "npm"
cache-dependency-path: src/frontend/package-lock.json
- name: Install dependencies
run: cd src/frontend/ && npm ci
- name: Extract the frontend translation
run: make frontend-i18n-extract
- name: Upload files to Crowdin
uses: crowdin/github-action@v2
with:
config: crowdin/config.yml
upload_sources: true
upload_translations: true
download_translations: false
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
CROWDIN_BASE_PATH: ${{ github.workspace }}
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
+102 -84
View File
@@ -251,92 +251,110 @@ These are the environmental options available on meet backend.
| Option | Description | default |
| ----------------------------------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| DJANGO_ALLOWED_HOSTS | Host that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | | false |
| DATA_DIR | Data directory location | /data |
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
| DJANGO_SECRET_KEY | Secret key used for Django security | |
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
| DB_NAME | name of the database | meet |
| DB_USER | user used to connect to database | dinum |
| DB_PASSWORD | password used to connect to the database | pass |
| DB_HOST | hostname of the database | localhost |
| DB_PORT | port to connect to database | 5432 |
| DB_NAME | Name of the database | meet |
| DB_USER | User used to connect to database | dinum |
| DB_PASSWORD | Password used to connect to the database | pass |
| DB_HOST | Hostname of the database | localhost |
| DB_PORT | Port to connect to database | 5432 |
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
| AWS_S3_ACCESS_KEY_ID | s3 access key | |
| AWS_S3_SECRET_ACCESS_KEY | s3 secret key | |
| AWS_S3_REGION_NAME | s3 region | |
| AWS_STORAGE_BUCKET_NAME | s3 bucket name | |
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
| AWS_S3_REGION_NAME | S3 region | |
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
| DJANGO_LANGUAGE_CODE | Default language | en-us |
| REDIS_URL | redis endpoint | redis://redis:6379/1 |
| REQUEST_ENTRY_THROTTLE_RATES | throttle rates | 150/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | deploy check | False |
| FRONTEND_ANALYTICS | analytics information | {} |
| FRONTEND_SUPPORT | crisp frontend support | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | silence livekit debug | false |
| DJANGO_EMAIL_BACKEND | email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_HOST | host of the email server | |
| DJANGO_EMAIL_HOST_USER | user to connect to the email server | |
| DJANGO_EMAIL_HOST_PASSWORD | password to connect tto the email server | |
| DJANGO_EMAIL_PORT | por tot connect to the email server | |
| DJANGO_EMAIL_USE_TLS | enable tls on email connection | false |
| DJANGO_EMAIL_USE_SSL | enable tls on email connection | false |
| DJANGO_EMAIL_FROM | email from account | from@example.com |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | allow all cors origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | origins to allow in string | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | origins to allow in regex | [] |
| SENTRY_DSN | sentry server | |
| DJANGO_CELERY_BROKER_URL | celery broker host | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | celery broker options | {} |
| OIDC_CREATE_USER | create oidc user if not exists | false |
| OIDC_VERIFY_SSL | verify ssl for oidc | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | fallback to email for identification | false |
| OIDC_RP_SIGN_ALGO | token verification algoritm used by oidc | RS256 |
| OIDC_RP_CLIENT_ID | oidc client | meet |
| OIDC_RP_CLIENT_SECRET | oidc client secret | |
| OIDC_OP_JWKS_ENDPOINT | oidc endpoint for JWKS | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | oidc endpoint for authorization | |
| OIDC_OP_TOKEN_ENDPOINT | oidc endpoint for token | |
| OIDC_OP_USER_ENDPOINT | oidc endpoint for user | |
| OIDC_OP_USER_ENDPOINT_FORMAT | oidc endpoint format (AUTO, JWT, JSON) | AUTO |
| OIDC_OP_LOGOUT_ENDPOINT | oidc endpoint for logout | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | extra parameters for oidc request | |
| OIDC_RP_SCOPES | oidc scopes | openid email |
| LOGIN_REDIRECT_URL | login redirect url | |
| LOGIN_REDIRECT_URL_FAILURE | login redurect url for failure | |
| LOGOUT_REDIRECT_URL | url to redirect to on logout | |
| OIDC_USE_NONCE | use nonce for oidc | true |
| OIDC_REDIRECT_REQUIRE_HTTPS | require https for oidc | false |
| OIDC_REDIRECT_ALLOWED_HOSTS | allowed redirect hosts for oidc | [] |
| OIDC_STORE_ID_TOKEN | store oidc ID token | true |
| ALLOW_LOGOUT_GET_METHOD | allow logout through get method | true |
| OIDC_REDIRECT_FIELD_NAME | direct field for oidc | returnTo |
| OIDC_USERINFO_FULLNAME_FIELDS | full name claim from OIDC token | ["given_name", "usual_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | shortname claim from OIDC token | given_name |
| OIDC_USERINFO_ESSENTIAL_CLAIMS | required claims from OIDC token | [] |
| LIVEKIT_API_KEY | livekit api key | |
| LIVEKIT_API_SECRET | livekit api secret | |
| LIVEKIT_API_URL | livekit api url | |
| RESOURCE_DEFAULT_ACCESS_LEVEL | default resource access level for rooms | public |
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
| FRONTEND_ANALYTICS | Analytics information | {} |
| FRONTEND_SUPPORT | Crisp frontend support configuration | {} |
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
| DJANGO_EMAIL_HOST | Host of the email server | |
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
| EMAIL_BRAND_NAME | Email branding name | |
| EMAIL_SUPPORT_EMAIL | Support email address | |
| EMAIL_LOGO_IMG | Email logo image | |
| EMAIL_DOMAIN | Email domain | |
| EMAIL_APP_BASE_URL | Email app base URL | |
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | true |
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
| SENTRY_DSN | Sentry server DSN | |
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
| OIDC_RP_SCOPES | OIDC scopes | openid email |
| OIDC_USE_NONCE | Use nonce for OIDC | true |
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
| LOGIN_REDIRECT_URL | Login redirect URL | |
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
| LIVEKIT_API_KEY | LiveKit API key | |
| LIVEKIT_API_SECRET | LiveKit API secret | |
| LIVEKIT_API_URL | LiveKit API URL | |
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
| RECORDING_ENABLE | record meeting option | false |
| RECORDING_OUTPUT_FOLDER | folder to store meetings | recordings |
| RECORDING_WORKER_CLASSES | worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
| RECORDING_EVENT_PARSER_CLASS | storage event engine for recording | core.recording.event.parsers.MinioParser |
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | enable storage event authorization | true |
| RECORDING_STORAGE_EVENT_ENABLE | enable recording storage events | false |
| RECORDING_STORAGE_EVENT_TOKEN | recording storage event token | |
| SUMMARY_SERVICE_ENDPOINT | summary service endpoint | |
| SUMMARY_SERVICE_API_TOKEN | api token voor summary service | |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | signup users to marketing emails | false |
| MARKETING_SERVICE_CLASS | markering class | core.services.marketing.BrevoMarketingService |
| BREVO_API_KEY | breva api key for marketing emails | |
| BREVO_API_CONTACT_LIST_IDS | brevo api contact list ids | [] |
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | brevo contact attributes | {"VISIO_USER": True} |
| BREVO_API_TIMEOUT | brevo timeout | 1 |
| LOBBY_KEY_PREFIX | lobby prefix | room_lobby |
| LOBBY_WAITING_TIMEOUT | lobby waiting tumeout | 3 |
| LOBBY_DENIED_TIMEOUT | lobby deny timeout | 5 |
| LOBBY_ACCEPTED_TIMEOUT | lobby accept timeout | 21600 |
| LOBBY_NOTIFICATION_TYPE | lobby notification types | participantWaiting |
| LOBBY_COOKIE_NAME | lobby cookie name | lobbyParticipantId |
| RECORDING_ENABLE | Record meeting option | false |
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
| RECORDING_EVENT_PARSER_CLASS | Storage event engine for recording | core.recording.event.parsers.MinioParser |
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | Enable storage event authorization | true |
| RECORDING_STORAGE_EVENT_ENABLE | Enable recording storage events | false |
| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | |
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
| BREVO_API_KEY | Brevo API key for marketing emails | |
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
+30 -1
View File
@@ -99,6 +99,7 @@ class ResourceAccessInline(admin.TabularInline):
model = models.ResourceAccess
extra = 0
autocomplete_fields = ["user"]
@admin.register(models.Room)
@@ -106,6 +107,10 @@ class RoomAdmin(admin.ModelAdmin):
"""Room admin interface declaration."""
inlines = (ResourceAccessInline,)
search_fields = ["name", "slug", "=id"]
list_display = ["name", "slug", "access_level", "created_at"]
list_filter = ["access_level", "created_at"]
readonly_fields = ["id", "created_at", "updated_at"]
class RecordingAccessInline(admin.TabularInline):
@@ -120,4 +125,28 @@ class RecordingAdmin(admin.ModelAdmin):
"""Recording admin interface declaration."""
inlines = (RecordingAccessInline,)
list_display = ("id", "status", "room", "created_at", "worker_id")
search_fields = ["status", "=id", "worker_id", "room__slug", "=room__id"]
list_display = ("id", "status", "room", "get_owner", "created_at", "worker_id")
list_filter = ["status", "room", "created_at"]
readonly_fields = ["id", "created_at", "updated_at"]
def get_queryset(self, request):
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
return super().get_queryset(request).prefetch_related("accesses__user")
def get_owner(self, obj):
"""Return the owner of the recording for display in the admin list."""
owners = [
access
for access in obj.accesses.all()
if access.role == models.RoleChoices.OWNER
]
if not owners:
return _("No owner")
if len(owners) > 1:
return _("Multiple owners")
return str(owners[0].user)
+1
View File
@@ -42,6 +42,7 @@ 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)
+1 -1
View File
@@ -37,7 +37,7 @@ class ResourceAccessSerializerMixin:
# Update
self.instance
and (
data["role"] == models.RoleChoices.OWNER
data.get("role") == models.RoleChoices.OWNER
and not self.instance.resource.is_owner(user)
or self.instance.role == models.RoleChoices.OWNER
and not self.instance.user == user
+6 -2
View File
@@ -10,6 +10,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.utils.module_loading import import_string
import brevo_python
import urllib3
logger = logging.getLogger(__name__)
@@ -120,8 +121,11 @@ class BrevoMarketingService:
try:
response = contact_api.create_contact(contact, **api_configurations)
except brevo_python.rest.ApiException as err:
logger.exception("Failed to create contact in Brevo")
except (
brevo_python.rest.ApiException,
urllib3.exceptions.ReadTimeoutError,
) as err:
logger.warning("Failed to create contact in Brevo", exc_info=True)
raise ContactCreationError("Failed to create contact in Brevo") from err
return response
@@ -11,6 +11,7 @@ from django.core.exceptions import ImproperlyConfigured
import brevo_python
import pytest
import urllib3
from core.services.marketing import (
BrevoMarketingService,
@@ -133,6 +134,34 @@ def test_create_contact_api_error(mock_contact_api):
brevo_service.create_contact(valid_contact_data)
@mock.patch("brevo_python.ContactsApi")
def test_create_contact_timeout_error(mock_contact_api):
"""Test contact creation timeout error handling."""
mock_api = mock_contact_api.return_value
settings.BREVO_API_KEY = "test-api-key"
settings.BREVO_API_CONTACT_LIST_IDS = [1, 2, 3, 4]
settings.BREVO_API_CONTACT_ATTRIBUTES = {"source": "test"}
valid_contact_data = ContactData(
email="test@example.com",
attributes={"first_name": "Test"},
list_ids=[1, 2],
update_enabled=True,
)
brevo_service = BrevoMarketingService()
mock_api.create_contact.side_effect = urllib3.exceptions.ReadTimeoutError(
pool=mock.Mock(),
url="https://api.brevo.com/v3/endpoint",
message="HTTPSConnectionPool(host='api.brevo.com', port=443): Read timed out.",
)
with pytest.raises(ContactCreationError, match="Failed to create contact in Brevo"):
brevo_service.create_contact(valid_contact_data)
@pytest.fixture
def clear_marketing_cache():
"""Clear marketing service cache between tests."""
+6 -6
View File
@@ -302,25 +302,25 @@ def test_pin_generation_max_retries(mock_randbelow, mock_logger, settings):
RoomFactory(pin_code="12345")
# Assert default max retries is low, 3
# Assert default max retries is low, 5
room1 = RoomFactory()
assert mock_randbelow.call_count == 3
assert mock_randbelow.call_count == 5
assert room1.pin_code is None
mock_logger.assert_called_once_with(
"Failed to generate unique PIN code of length %s after %s attempts", 5, 3
"Failed to generate unique PIN code of length %s after %s attempts", 5, 5
)
mock_logger.reset_mock()
mock_randbelow.reset_mock()
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES = 5
settings.ROOM_TELEPHONY_PIN_MAX_RETRIES = 3
room2 = RoomFactory()
assert mock_randbelow.call_count == 5
assert mock_randbelow.call_count == 3
assert room2.pin_code is None
mock_logger.assert_called_once_with(
"Failed to generate unique PIN code of length %s after %s attempts", 5, 5
"Failed to generate unique PIN code of length %s after %s attempts", 5, 3
)
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,6 +29,14 @@ msgstr "Berechtigungen"
msgid "Important dates"
msgstr "Wichtige Daten"
#: core/admin.py:143
msgid "No owner"
msgstr "Kein Eigentümer"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Mehrere Eigentümer"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,6 +29,14 @@ msgstr "Permissions"
msgid "Important dates"
msgstr "Important dates"
#: core/admin.py:143
msgid "No owner"
msgstr "No owner"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Multiple owners"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr "You must be administrator or owner of a room to add accesses to it."
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,6 +29,14 @@ msgstr "Permissions"
msgid "Important dates"
msgstr "Dates importantes"
#: core/admin.py:143
msgid "No owner"
msgstr "Pas de propriétaire"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Plusieurs propriétaires"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-16 17:04+0000\n"
"POT-Creation-Date: 2025-05-26 10:50+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -29,6 +29,14 @@ msgstr "Rechten"
msgid "Important dates"
msgstr "Belangrijke datums"
#: core/admin.py:143
msgid "No owner"
msgstr "Geen eigenaar"
#: core/admin.py:146
msgid "Multiple owners"
msgstr "Meerdere eigenaren"
#: core/api/serializers.py:63
msgid "You must be administrator or owner of a room to add accesses to it."
msgstr ""
+16 -6
View File
@@ -314,11 +314,14 @@ class Base(Configuration):
False, environ_name="FRONTEND_SILENCE_LIVEKIT_DEBUG", environ_prefix=None
),
"is_silent_login_enabled": values.BooleanValue(
True, environ_name="FRONTEND_IS_SILENT_LOGING_ENABLED", environ_prefix=None
True, environ_name="FRONTEND_IS_SILENT_LOGIN_ENABLED", environ_prefix=None
),
"feedback": values.DictValue(
{}, environ_name="FRONTEND_FEEDBACK", environ_prefix=None
),
"transcript": values.DictValue(
{}, environ_name="FRONTEND_TRANSCRIPT", environ_prefix=None
)
}
# Mail
@@ -360,7 +363,9 @@ class Base(Configuration):
# Session
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "default"
SESSION_COOKIE_AGE = 60 * 60 * 12
SESSION_COOKIE_AGE = values.PositiveIntegerValue(
default=60 * 60 * 12, environ_name="SESSION_COOKIE_AGE", environ_prefix=None
)
# OIDC - Authorization Code Flow
OIDC_AUTHENTICATE_CLASS = "lasuite.oidc_login.views.OIDCAuthenticationRequestView"
@@ -636,10 +641,7 @@ class Base(Configuration):
release=get_release(),
integrations=[DjangoIntegration()],
)
# Add the application name to the Sentry scope
scope = sentry_sdk.get_global_scope()
scope.set_tag("application", "backend")
sentry_sdk.set_tag("application", "backend")
# Ignore the logs added by the DockerflowMiddleware
ignore_logger("request.summary")
@@ -803,6 +805,14 @@ class Staging(Production):
"""
class Sandbox(Production):
"""
Sandbox environment settings
nota bene: it should inherit from the Production environment.
"""
class PreProduction(Production):
"""
Pre-production environment settings
+1 -1
View File
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "meet"
version = "0.1.22"
version = "0.1.23"
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
classifiers = [
"Development Status :: 5 - Production/Stable",
+1 -1
View File
@@ -35,7 +35,7 @@ ENV VITE_API_BASE_URL=${VITE_API_BASE_URL}
RUN npm run build
# ---- Front-end image ----
FROM nginxinc/nginx-unprivileged:1.26-alpine AS frontend-production
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
USER root
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2
+37 -25
View File
@@ -1,14 +1,14 @@
{
"name": "meet",
"version": "0.1.22",
"version": "0.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "meet",
"version": "0.1.22",
"version": "0.1.23",
"dependencies": {
"@livekit/components-react": "2.9.3",
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.5",
"@livekit/track-processors": "0.5.6",
"@pandacss/preset-panda": "0.53.6",
@@ -990,13 +990,13 @@
}
},
"node_modules/@floating-ui/dom": {
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
"integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
"version": "1.6.11",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.11.tgz",
"integrity": "sha512-qkMCxSR24v2vGkhYDo/UzxfJN3D4syqSjyuTFz6C7XcpU1pASPRieNI0Kj5VP3/503mOfYiGY891ugBX1GlABQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.6.0",
"@floating-ui/utils": "^0.2.9"
"@floating-ui/utils": "^0.2.8"
}
},
"node_modules/@floating-ui/utils": {
@@ -1214,39 +1214,48 @@
}
},
"node_modules/@livekit/components-core": {
"version": "0.12.4",
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.4.tgz",
"integrity": "sha512-a/GkK8XFULPhXoSKxuXEU62gwTAYJ83DP5/vlRzwESEY+rsoiw2NvvPZtDCU17yyd/5QBIF9VdDjB9ZZF0dOfQ==",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@floating-ui/dom": "1.6.13",
"@floating-ui/dom": "1.6.11",
"loglevel": "1.9.1",
"rxjs": "7.8.2"
"rxjs": "7.8.1"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"livekit-client": "^2.11.1",
"livekit-client": "^2.8.1",
"tslib": "^2.6.2"
}
},
"node_modules/@livekit/components-react": {
"version": "2.9.3",
"resolved": "https://registry.npmjs.org/@livekit/components-react/-/components-react-2.9.3.tgz",
"integrity": "sha512-gE1sEE57BkBz3+TQHrOXVDVwVMwV5wtIYokdrfd7vshh22/PtWWj3vON9wzYLFRKx98L6QyAzyh7W9EWu3Lj9Q==",
"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": {
"@livekit/components-core": "0.12.4",
"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==",
"license": "Apache-2.0",
"dependencies": {
"@livekit/components-core": "0.12.1",
"clsx": "2.1.1",
"usehooks-ts": "3.1.1"
"usehooks-ts": "3.1.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@livekit/krisp-noise-filter": "^0.2.12",
"livekit-client": "^2.11.1",
"livekit-client": "^2.8.1",
"react": ">=18",
"react-dom": ">=18",
"tslib": "^2.6.2"
@@ -7385,7 +7394,8 @@
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="
"integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
@@ -7418,6 +7428,7 @@
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.1.tgz",
"integrity": "sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
},
@@ -8661,6 +8672,7 @@
"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"
}
@@ -9490,9 +9502,9 @@
}
},
"node_modules/usehooks-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz",
"integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.0.tgz",
"integrity": "sha512-bBIa7yUyPhE1BCc0GmR96VU/15l/9gP1Ch5mYdLcFBaFGQsdmXkvjV0TtOqW1yUd6VjIwDunm+flSciCQXujiw==",
"license": "MIT",
"dependencies": {
"lodash.debounce": "^4.0.8"
@@ -9501,7 +9513,7 @@
"node": ">=16.15.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
"react": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/util-deprecate": {
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "meet",
"private": true,
"version": "0.1.22",
"version": "0.1.23",
"type": "module",
"scripts": {
"dev": "panda codegen && vite",
@@ -13,7 +13,7 @@
"check": "prettier --check ./src"
},
"dependencies": {
"@livekit/components-react": "2.9.3",
"@livekit/components-react": "2.8.1",
"@livekit/components-styles": "1.1.5",
"@livekit/track-processors": "0.5.6",
"@pandacss/preset-panda": "0.53.6",
+7
View File
@@ -10,12 +10,19 @@ 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[]
@@ -2,19 +2,33 @@ import { silenceLiveKitLogs } from '@/utils/livekit'
import { useConfig } from '@/api/useConfig'
import { useAnalytics } from '@/features/analytics/hooks/useAnalytics'
import { useSupport } from '@/features/support/hooks/useSupport'
import { useSyncUserPreferencesWithBackend } from '@/features/auth'
import { useEffect } from 'react'
export const AppInitialization = () => {
const { data } = useConfig()
useSyncUserPreferencesWithBackend()
const {
analytics = {},
support = {},
silence_livekit_debug_logs = false,
custom_css = '',
} = 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,8 +10,7 @@ export const NotFoundScreen = () => {
<Screen layout="centered">
<CenteredContent title={t('notFound.heading')} withBackButton>
<Text centered>
{t('notFound.body')}{' '}
<Bold>https://visio.numerique.gouv.fr/xxx-yyyy-zzz.</Bold>
{t('notFound.body')} <Bold>{window.origin}/xxx-yyyy-zzz.</Bold>
</Text>
</CenteredContent>
</Screen>
@@ -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 { BETA_USERS_FORM_URL } from '@/utils/constants'
import { useConfig } from '@/api/useConfig'
const Heading = styled('h2', {
base: {
@@ -171,6 +171,8 @@ export const IntroSlider = () => {
const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
const NUMBER_SLIDES = SLIDES.length
const { data } = useConfig()
return (
<Container>
<div
@@ -203,7 +205,7 @@ export const IntroSlider = () => {
<Body>{t(`${slide.key}.body`)}</Body>
{slide.isAvailableInBeta && (
<LinkButton
href={BETA_USERS_FORM_URL}
href={data?.transcript.form_beta_users}
target="_blank"
tooltip={t('beta.tooltip')}
variant={'primary'}
@@ -23,7 +23,7 @@ export const JoinMeetingDialog = () => {
name="roomId"
label={t('joinInputLabel')}
description={t('joinInputExample', {
example: 'https://visio.numerique.gouv.fr/azer-tyu-qsdf',
example: window.origin + '/azer-tyu-qsdf',
})}
validate={(value) => {
return !isRoomValid(value.trim()) ? (
@@ -11,5 +11,6 @@ export const NotificationDuration = {
PARTICIPANT_JOINED: ToastDuration.LONG,
HAND_RAISED: ToastDuration.LONG,
LOWER_HAND: ToastDuration.EXTRA_LONG,
RECORDING_SAVING: ToastDuration.EXTRA_LONG,
REACTION_RECEIVED: ToastDuration.SHORT,
} as const
@@ -10,4 +10,5 @@ export enum NotificationType {
TranscriptionStopped = 'transcriptionStopped',
ScreenRecordingStarted = 'screenRecordingStarted',
ScreenRecordingStopped = 'screenRecordingStopped',
RecordingSaving = 'recordingSaving',
}
@@ -8,6 +8,8 @@ export interface ToastData {
participant: Participant
type: NotificationType
message?: string
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any
}
// Using a global queue for toasts allows for centralized management and queuing of notifications
@@ -0,0 +1,62 @@
import { useToast } from '@react-aria/toast'
import { useMemo, useRef } from 'react'
import { Text } from '@/primitives'
import { StyledToastContainer, ToastProps } from './Toast'
import { HStack } from '@/styled-system/jsx'
import { useTranslation } from 'react-i18next'
import { useUser } from '@/features/auth'
import { css } from '@/styled-system/css'
import { RecordingMode } from '@/features/recording'
export function ToastRecordingSaving({ state, ...props }: ToastProps) {
const { t } = useTranslation('notifications', { keyPrefix: 'recordingSave' })
const ref = useRef(null)
const { toastProps, contentProps } = useToast(props, state, ref)
const { user } = useUser()
const modeLabel = useMemo(() => {
const mode = props.toast.content.mode as RecordingMode
switch (mode) {
case RecordingMode.Transcript:
return 'transcript'
case RecordingMode.ScreenRecording:
return 'screenRecording'
}
}, [props.toast.content])
return (
<StyledToastContainer {...toastProps} ref={ref}>
<HStack
justify="center"
alignItems="center"
{...contentProps}
padding={14}
gap={1}
>
<Text
margin={false}
className={css({
maxWidth: '22rem',
wordBreak: 'break-word',
overflowWrap: 'break-word',
whiteSpace: 'normal',
})}
>
{user?.email ? (
<span
dangerouslySetInnerHTML={{
__html: t(`${modeLabel}.message`, {
email: user.email,
}),
}}
/>
) : (
t(`${modeLabel}.default`)
)}
</Text>
</HStack>
</StyledToastContainer>
)
}
@@ -10,6 +10,7 @@ import { ToastMuted } from './ToastMuted'
import { ToastMessageReceived } from './ToastMessageReceived'
import { ToastLowerHand } from './ToastLowerHand'
import { ToastAnyRecording } from './ToastAnyRecording'
import { ToastRecordingSaving } from './ToastRecordingSaving'
interface ToastRegionProps extends AriaToastRegionProps {
state: ToastState<ToastData>
@@ -43,6 +44,11 @@ const renderToast = (
case NotificationType.ScreenRecordingStopped:
return <ToastAnyRecording key={toast.key} toast={toast} state={state} />
case NotificationType.RecordingSaving:
return (
<ToastRecordingSaving key={toast.key} toast={toast} state={state} />
)
default:
return <Toast key={toast.key} toast={toast} state={state} />
}
@@ -1,2 +1,3 @@
export { useNotifyParticipants } from './hooks/useNotifyParticipants'
export { NotificationType } from './NotificationType'
export { notifyRecordingSaveInProgress } from './utils'
@@ -3,6 +3,7 @@ import { NotificationType } from './NotificationType'
import { NotificationDuration } from './NotificationDuration'
import { Participant } from 'livekit-client'
import { NotificationPayload } from './NotificationPayload'
import { RecordingMode } from '@/features/recording'
export const showLowerHandToast = (
participant: Participant,
@@ -49,3 +50,17 @@ export const decodeNotificationDataReceived = (
return
}
}
export const notifyRecordingSaveInProgress = (
mode: RecordingMode,
participant: Participant
) => {
toastQueue.add(
{
participant,
mode,
type: NotificationType.RecordingSaving,
},
{ timeout: NotificationDuration.RECORDING_SAVING }
)
}
@@ -106,14 +106,10 @@ export const RecordingStateToast = () => {
switch (recordingSnap.status) {
case RecordingStatus.TRANSCRIPT_STARTED:
return 'transcript.started'
case RecordingStatus.TRANSCRIPT_STOPPING:
return 'transcript.stopping'
case RecordingStatus.TRANSCRIPT_STARTING:
return 'transcript.starting'
case RecordingStatus.SCREEN_RECORDING_STARTED:
return 'screenRecording.started'
case RecordingStatus.SCREEN_RECORDING_STOPPING:
return 'screenRecording.stopping'
case RecordingStatus.SCREEN_RECORDING_STARTING:
return 'screenRecording.starting'
case RecordingStatus.ANY_STARTED:
@@ -14,17 +14,19 @@ 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,
notifyRecordingSaveInProgress,
useNotifyParticipants,
} from '@/features/notifications'
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' })
@@ -84,6 +86,10 @@ export const ScreenRecordingSidePanel = () => {
await notifyParticipants({
type: NotificationType.ScreenRecordingStopped,
})
notifyRecordingSaveInProgress(
RecordingMode.ScreenRecording,
room.localParticipant
)
} else {
await startRecordingRoom({
id: roomId,
@@ -194,9 +200,11 @@ export const ScreenRecordingSidePanel = () => {
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE_RECORDING} target="_blank">
{t('start.linkMore')}
</A>
{data?.support?.help_article_recording && (
<A href={data.support.help_article_recording} target="_blank">
{t('start.linkMore')}
</A>
)}
</Text>
<Button
isDisabled={isDisabled}
@@ -10,25 +10,26 @@ import {
useIsRecordingTransitioning,
useStartRecording,
useStopRecording,
useHasFeatureWithoutAdminRights,
} from '../index'
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,
useNotifyParticipants,
notifyRecordingSaveInProgress,
} from '@/features/notifications'
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' })
@@ -42,6 +43,12 @@ export const TranscriptSidePanel = () => {
RecordingMode.Transcript,
FeatureFlags.Transcript
)
const hasFeatureWithoutAdminRights = useHasFeatureWithoutAdminRights(
RecordingMode.Transcript,
FeatureFlags.Transcript
)
const roomId = useRoomId()
const { mutateAsync: startRecordingRoom, isPending: isPendingToStart } =
@@ -92,6 +99,10 @@ export const TranscriptSidePanel = () => {
await notifyParticipants({
type: NotificationType.TranscriptionStopped,
})
notifyRecordingSaveInProgress(
RecordingMode.Transcript,
room.localParticipant
)
} else {
await startRecordingRoom({ id: roomId, mode: RecordingMode.Transcript })
recordingStore.status = RecordingStatus.TRANSCRIPT_STARTING
@@ -134,30 +145,64 @@ export const TranscriptSidePanel = () => {
/>
{!hasTranscriptAccess ? (
<>
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('beta.body')}{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={BETA_USERS_FORM_URL}
target="_blank"
>
{t('beta.button')}
</LinkButton>
{hasFeatureWithoutAdminRights ? (
<>
<Text>{t('notAdminOrOwner.heading')}</Text>
<Text
variant="note"
wrap="balance"
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('notAdminOrOwner.body')}
<br />
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('notAdminOrOwner.linkMore')}
</A>
)}
</Text>
</>
) : (
<>
<Text>{t('beta.heading')}</Text>
<Text
variant="note"
wrap={'pretty'}
centered
className={css({
textStyle: 'sm',
marginBottom: '2.5rem',
marginTop: '0.25rem',
})}
>
{t('beta.body')}{' '}
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('start.linkMore')}
</A>
)}
</Text>
<LinkButton
size="sm"
variant="tertiary"
href={data?.transcript.form_beta_users}
target="_blank"
>
{t('beta.button')}
</LinkButton>
</>
)}
</>
) : (
<>
@@ -227,9 +272,14 @@ export const TranscriptSidePanel = () => {
})}
>
{t('start.body')} <br />{' '}
<A href={CRISP_HELP_ARTICLE_TRANSCRIPT} target="_blank">
{t('start.linkMore')}
</A>
{data?.support?.help_article_transcript && (
<A
href={data.support.help_article_transcript}
target="_blank"
>
{t('start.linkMore')}
</A>
)}
</Text>
<Button
isDisabled={isDisabled}
@@ -0,0 +1,22 @@
import { useFeatureFlagEnabled } from 'posthog-js/react'
import { useIsAnalyticsEnabled } from '@/features/analytics/hooks/useIsAnalyticsEnabled'
import { RecordingMode } from '../types'
import { useIsRecordingModeEnabled } from './useIsRecordingModeEnabled'
import { useIsAdminOrOwner } from '@/features/rooms/livekit/hooks/useIsAdminOrOwner'
import { FeatureFlags } from '@/features/analytics/enums'
export const useHasFeatureWithoutAdminRights = (
mode: RecordingMode,
featureFlag: FeatureFlags
) => {
const featureEnabled = useFeatureFlagEnabled(featureFlag)
const isAnalyticsEnabled = useIsAnalyticsEnabled()
const isRecordingModeEnabled = useIsRecordingModeEnabled(mode)
const isAdminOrOwner = useIsAdminOrOwner()
return (
(featureEnabled || !isAnalyticsEnabled) &&
isRecordingModeEnabled &&
!isAdminOrOwner
)
}
@@ -3,6 +3,7 @@ export { useIsRecordingModeEnabled } from './hooks/useIsRecordingModeEnabled'
export { useIsRecordingTransitioning } from './hooks/useIsRecordingTransitioning'
export { useHasRecordingAccess } from './hooks/useHasRecordingAccess'
export { useIsRecordingActive } from './hooks/useIsRecordingActive'
export { useHasFeatureWithoutAdminRights } from './hooks/useHasFeatureWithoutAdminRights'
// api
export { useStartRecording } from './api/startRecording'
@@ -2,7 +2,6 @@ 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'
@@ -15,6 +14,7 @@ import {
useIsRecordingActive,
} from '@/features/recording'
import { FeatureFlags } from '@/features/analytics/enums'
import { useConfig } from '@/api/useConfig'
export interface ToolsButtonProps {
icon: ReactNode
@@ -113,6 +113,7 @@ const ToolButton = ({
}
export const Tools = () => {
const { data } = useConfig()
const { openTranscript, openScreenRecording, activeSubPanelId } =
useSidePanel()
const { t } = useTranslation('rooms', { keyPrefix: 'moreTools' })
@@ -160,10 +161,14 @@ export const Tools = () => {
margin="md"
>
{t('body')}{' '}
<A href={CRISP_HELP_ARTICLE_MORE_TOOLS} target="_blank">
{t('moreLink')}
</A>
.
{data?.support?.help_article_more_tools && (
<>
<A href={data?.support?.help_article_more_tools} target="_blank">
{t('moreLink')}
</A>
.
</>
)}
</Text>
{isTranscriptEnabled && (
<ToolButton
@@ -29,5 +29,15 @@
"screenRecording": {
"started": "{{name}} hat die Aufzeichnung des Treffens gestartet.",
"stopped": "{{name}} hat die Aufzeichnung des Treffens gestoppt."
},
"recordingSave": {
"transcript": {
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald die Transkription fertig ist.",
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald die Transkription fertig ist."
},
"screenRecording": {
"message": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail an <strong>{{email}}</strong>, sobald sie fertig ist.",
"default": "Wir finalisieren Ihre Aufnahme! Sie erhalten eine E-Mail, sobald sie fertig ist."
}
}
}
+13 -3
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": {
@@ -223,6 +223,11 @@
"loading": "Transkription wird gestartet",
"linkMore": "Mehr erfahren"
},
"notAdminOrOwner": {
"heading": "Zugriff eingeschränkt",
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine Transkription (Beta) starten.",
"linkMore": "Mehr erfahren"
},
"stop": {
"heading": "Transkription läuft...",
"body": "Die Transkription deines Meetings läuft. Du erhältst das Ergebnis per E-Mail, sobald das Meeting beendet ist.",
@@ -230,7 +235,7 @@
},
"stopping": {
"heading": "Daten werden gespeichert…",
"body": "Dieser Vorgang kann einige Minuten dauern. Danke für deine Geduld."
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
},
"beta": {
"heading": "Werde Beta-Tester",
@@ -254,9 +259,14 @@
"loading": "Aufzeichnung wird gestartet",
"linkMore": "Mehr erfahren"
},
"notAdminOrOwner": {
"heading": "Zugriff eingeschränkt",
"body": "Aus Sicherheitsgründen kann nur der Ersteller oder ein Administrator des Meetings eine videoaufnahme (Beta) starten.",
"linkMore": "Mehr erfahren"
},
"stopping": {
"heading": "Daten werden gespeichert…",
"body": "Dieser Vorgang kann einige Minuten dauern. Danke für deine Geduld."
"body": "Sie können das Meeting verlassen, wenn Sie möchten; die Aufzeichnung wird automatisch beendet."
},
"stop": {
"heading": "Aufzeichnung läuft…",
@@ -29,5 +29,15 @@
"screenRecording": {
"started": "{{name}} started the meeting recording.",
"stopped": "{{name}} stopped the meeting recording."
},
"recordingSave": {
"transcript": {
"message": "Your recording is being saved! Well send the transcript to <strong>{{email}}</strong> as soon as its ready.",
"default": "Your recording is being saved! Well send the transcript to your email as soon as its ready."
},
"screenRecording": {
"message": "Your recording is being saved! Well send a notification to <strong>{{email}}</strong> as soon as its ready.",
"default": "Your recording is being saved! Well send a notification to your email as soon as its ready."
}
}
}
+13 -3
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": {
@@ -223,6 +223,11 @@
"loading": "Transcription starting",
"linkMore": "Learn more"
},
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a transcription (beta).",
"linkMore": "Learn more"
},
"stop": {
"heading": "Transcription in progress...",
"body": "The transcription of your meeting is in progress. You will receive the result by email once the meeting is finished.",
@@ -230,7 +235,7 @@
},
"stopping": {
"heading": "Saving your data…",
"body": "This process may take a few minutes. Thank you for your patience."
"body": "You can leave the meeting if you wish; the recording will finish automatically."
},
"beta": {
"heading": "Become a beta tester",
@@ -254,9 +259,14 @@
"loading": "Recording starting",
"linkMore": "Learn more"
},
"notAdminOrOwner": {
"heading": "Restricted Access",
"body": "For security reasons, only the meeting creator or an admin can start a recording (beta).",
"linkMore": "Learn more"
},
"stopping": {
"heading": "Saving your data…",
"body": "This process may take a few minutes. Thank you for your patience."
"body": "You can leave the meeting if you wish; the recording will finish automatically."
},
"stop": {
"heading": "Recording in progress…",
@@ -29,5 +29,15 @@
"screenRecording": {
"started": "{{name}} a démarré l'enregistrement de la réunion.",
"stopped": "{{name}} a arrêté l'enregistrement de la réunion."
},
"recordingSave": {
"transcript": {
"message": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail à <strong>{{email}}</strong> dès que la transcription sera prête.",
"default": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail dès que la transcription sera prête."
},
"screenRecording": {
"message": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail à <strong>{{email}}</strong> dès quil sera prêt.",
"default": "Nous finalisons votre enregistrement ! Vous recevrez un e-mail dès quil sera prêt."
}
}
}
+16 -6
View File
@@ -20,7 +20,7 @@
},
"heading": "Rejoindre la réunion ?",
"effects": {
"description": "Appliquer des effets",
"description": "Effets d'arrière plan",
"title": "Effets",
"subTitle": "Paramétrez les effets de votre caméra."
},
@@ -128,7 +128,7 @@
"feedback": "Partager votre avis",
"settings": "Paramètres",
"username": "Choisir votre nom",
"effects": "Appliquer des effets",
"effects": "Effets d'arrière plan",
"switchCamera": "Changer de caméra",
"fullscreen": {
"enter": "Plein écran",
@@ -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": {
@@ -223,6 +223,11 @@
"loading": "Démarrage de la transcription",
"linkMore": "En savoir plus"
},
"notAdminOrOwner": {
"heading": "Accès restreint",
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer une transcription (beta).",
"linkMore": "En savoir plus"
},
"stop": {
"heading": "Transcription en cours …",
"body": "La transcription de votre réunion est en cours. Vous recevrez le resultat par email une fois la réunion terminée.",
@@ -230,7 +235,7 @@
},
"stopping": {
"heading": "Sauvegarde de vos données…",
"body": "Cette opération peut durer quelques minutes. Merci de votre patience."
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
},
"beta": {
"heading": "Devenez beta testeur",
@@ -254,9 +259,14 @@
"loading": "Démarrage de l'enregistrement",
"linkMore": "En savoir plus"
},
"notAdminOrOwner": {
"heading": "Accès restreint",
"body": "Pour des raisons de sécurité, seul le créateur ou un administrateur de la réunion peut lancer un enregistrement (beta).",
"linkMore": "En savoir plus"
},
"stopping": {
"heading": "Sauvegarde de vos données…",
"body": "Cette opération peut durer quelques minutes. Merci de votre patience."
"body": "Vous pouvez quitter la réunion si vous le souhaitez, la sauvegarde se terminera automatiquement."
},
"stop": {
"heading": "Enregistrement en cours …",
@@ -371,7 +381,7 @@
"enable": "Épingler",
"disable": "Annuler l'épinglage"
},
"effects": "Appliquer des effets",
"effects": "Effets d'arrière plan",
"muteParticipant": "Couper le micro de {{name}}",
"fullScreen": "Plein écran"
},
@@ -29,5 +29,15 @@
"screenRecording": {
"started": "{{name}} is begonnen met het opnemen van de vergadering.",
"stopped": "{{name}} is gestopt met het opnemen van de vergadering."
},
"recordingSave": {
"transcript": {
"message": "We zijn uw opname aan het voltooien! U ontvangt een e-mail op <strong>{{email}}</strong> zodra de transcriptie klaar is.",
"default": "We zijn uw opname aan het voltooien! U ontvangt een e-mail zodra de transcriptie klaar is."
},
"screenRecording": {
"message": "We zijn uw opname aan het voltooien! U ontvangt een e-mail op <strong>{{email}}</strong> zodra deze klaar is.",
"default": "We zijn uw opname aan het voltooien! U ontvangt een e-mail zodra deze klaar is."
}
}
}
+13 -3
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": {
@@ -223,6 +223,11 @@
"loading": "Transcriptie begint",
"linkMore": "Meer informatie"
},
"notAdminOrOwner": {
"heading": "Toegang beperkt",
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een transcriptie starten (beta).",
"linkMore": "Meer informatie"
},
"stop": {
"heading": "Transcriptie bezig...",
"body": "De transcriptie van uw vergadering is bezig. U ontvangt het resultaat per e-mail zodra de vergadering is afgelopen.",
@@ -230,7 +235,7 @@
},
"stopping": {
"heading": "Uw gegevens worden opgeslagen…",
"body": "Dit proces kan enkele minuten duren. Bedankt voor uw geduld."
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
},
"beta": {
"heading": "Word betatester",
@@ -254,9 +259,14 @@
"loading": "Opname gestarten",
"linkMore": "Meer informatie"
},
"notAdminOrOwner": {
"heading": "Toegang beperkt",
"body": "Om veiligheidsredenen kan alleen de maker of een beheerder van de vergadering een opname starten (beta).",
"linkMore": "Meer informatie"
},
"stopping": {
"heading": "Uw gegevens worden opgeslagen…",
"body": "Dit proces kan enkele minuten duren. Bedankt voor uw geduld."
"body": "U kunt de vergadering verlaten als u dat wilt; de opname wordt automatisch voltooid."
},
"stop": {
"heading": "Opname bezig …",
-11
View File
@@ -1,11 +0,0 @@
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,7 +54,8 @@ 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'}"
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_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
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v2
type: application
name: meet
version: 0.0.7
version: 0.0.8
+2 -2
View File
@@ -60,7 +60,7 @@ spec:
serviceName: {{ include "meet.frontend.fullname" . }}
servicePort: {{ .Values.frontend.service.port }}
{{- end }}
- path: /api/v
- path: /api/
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
@@ -96,7 +96,7 @@ spec:
serviceName: {{ include "meet.frontend.fullname" $ }}
servicePort: {{ $.Values.frontend.service.port }}
{{- end }}
- path: /api/v
- path: /api/
{{- if semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion }}
pathType: Prefix
{{- end }}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "mail_mjml",
"version": "0.1.22",
"version": "0.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mail_mjml",
"version": "0.1.22",
"version": "0.1.23",
"license": "MIT",
"dependencies": {
"@html-to/text-cli": "0.5.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mail_mjml",
"version": "0.1.22",
"version": "0.1.23",
"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.22",
"version": "0.1.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sdk",
"version": "0.1.22",
"version": "0.1.23",
"license": "ISC",
"workspaces": [
"./library",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sdk",
"version": "0.1.22",
"version": "0.1.23",
"author": "",
"license": "ISC",
"description": "",
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "summary"
version = "0.1.22"
version = "0.1.23"
dependencies = [
"fastapi[standard]>=0.105.0",
"uvicorn>=0.24.0",
+33 -8
View File
@@ -36,6 +36,28 @@ if settings.sentry_dsn and settings.sentry_is_enabled:
sentry_sdk.init(dsn=settings.sentry_dsn, enable_tracing=True)
DEFAULT_EMPTY_TRANSCRIPTION = """
**Aucun contenu audio na été détecté dans votre transcription.**
*Si vous pensez quil sagit dune erreur, nhésitez pas à contacter
notre support technique : visio@numerique.gouv.fr*
.
.
.
Quelques points que nous vous conseillons de vérifier :
- Un micro était-il activé ?
- Étiez-vous suffisamment proche ?
- Le micro est-il de bonne qualité ?
- Lenregistrement dure-t-il plus de 30 secondes ?
"""
def save_audio_stream(audio_stream, chunk_size=32 * 1024):
"""Save an audio stream to a temporary OGG file."""
with tempfile.NamedTemporaryFile(suffix=".ogg", delete=False) as tmp:
@@ -64,9 +86,9 @@ def format_segments(transcription_data):
conversation with speaker labels.
"""
formatted_output = ""
if not transcription_data or not hasattr(transcription_data, 'segments'):
if isinstance(transcription_data, dict) and 'segments' in transcription_data:
segments = transcription_data['segments']
if not transcription_data or not hasattr(transcription_data, "segments"):
if isinstance(transcription_data, dict) and "segments" in transcription_data:
segments = transcription_data["segments"]
else:
return "Error: Invalid transcription data format"
else:
@@ -75,8 +97,8 @@ def format_segments(transcription_data):
previous_speaker = None
for segment in segments:
speaker = segment.get('speaker', 'UNKNOWN_SPEAKER')
text = segment.get('text', '')
speaker = segment.get("speaker", "UNKNOWN_SPEAKER")
text = segment.get("text", "")
if text:
if speaker != previous_speaker:
formatted_output += f"\n\n **{speaker}**: {text}"
@@ -85,6 +107,7 @@ def format_segments(transcription_data):
previous_speaker = speaker
return formatted_output
def post_with_retries(url, data):
"""Send POST request with automatic retries."""
session = create_retry_session()
@@ -218,7 +241,11 @@ def process_audio_transcribe_summarize_v2(filename: str, email: str, sub: str):
os.remove(temp_file_path)
logger.debug("Temporary file removed: %s", temp_file_path)
formatted_transcription = format_segments(transcription)
formatted_transcription = (
DEFAULT_EMPTY_TRANSCRIPTION
if not transcription.segments
else format_segments(transcription)
)
data = {
"title": "Transcription",
@@ -236,5 +263,3 @@ def process_audio_transcribe_summarize_v2(filename: str, email: str, sub: str):
logger.debug("Response body: %s", response.text)
# TODO - integrate summarize the transcript and create a new document.