mirror of
https://github.com/suitenumerique/meet.git
synced 2026-07-27 12:19:10 +00:00
Compare commits
62 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50fb093d39 | |||
| 6d2c31eb0a | |||
| e9bdf173de | |||
| dd23ce817a | |||
| 1ca8f6e5ea | |||
| f98e884067 | |||
| b498926353 | |||
| 3d4dc2d631 | |||
| 1523e6aec9 | |||
| 99510c9c6a | |||
| 9f003e95f3 | |||
| 46c30b6fcd | |||
| 1533ae8a3c | |||
| 526e96797e | |||
| 8903a55008 | |||
| 2a60b49086 | |||
| aee1847303 | |||
| 6b7cd8ab2e | |||
| cc2a0ad322 | |||
| 3300226bbe | |||
| 8efb11b3ae | |||
| 8cf15cabaf | |||
| 9b7c449ca5 | |||
| 5c27aba00f | |||
| 7256b87511 | |||
| 7b7951a0e0 | |||
| 5c15140783 | |||
| 898bc9a0f8 | |||
| fd1715bacf | |||
| c9de7d049f | |||
| 6368b676a6 | |||
| 65789ef706 | |||
| c0feb1ee82 | |||
| 135b99aee7 | |||
| 040df0e15a | |||
| 00e197b216 | |||
| eae1a382d5 | |||
| d4a7cf279c | |||
| 70a296eea6 | |||
| ac85a20271 | |||
| 13036f6ab7 | |||
| 64819b3696 | |||
| 53722ad1bc | |||
| dcfdd35c82 | |||
| 16f465432d | |||
| 90f95ab2a9 | |||
| 61f7ad05e9 | |||
| 69a6dd1463 | |||
| 6d06aee92d | |||
| 54908b9caa | |||
| 08af6e77bb | |||
| 4525c9c255 | |||
| 44d3ed8f2e | |||
| 9522dc72ac | |||
| 09dbb250a7 | |||
| 321cb99f82 | |||
| 52bad6faaf | |||
| 0065527a5e | |||
| 818d20888e | |||
| f1fce4431f | |||
| 491866e584 | |||
| f777f2fdeb |
@@ -46,7 +46,7 @@ jobs:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
target: backend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -112,7 +112,7 @@ jobs:
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -139,7 +139,7 @@ jobs:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -159,7 +159,7 @@ jobs:
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -186,7 +186,7 @@ jobs:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -208,7 +208,7 @@ jobs:
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -235,7 +235,7 @@ jobs:
|
||||
images: lasuite/meet-agents
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
||||
+49
-1
@@ -8,6 +8,54 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) cap and paginate tiles in picture-in-picture #1383
|
||||
- 📝(docs) document rebranding the favicon via a volume mount #1443
|
||||
- ✨(backend) add command to clean pending and deleted files
|
||||
- 🧱(helm) run clean files command as cronjob
|
||||
- ✨(backend) add fallback to save recordings without S3/MinIO webhooks
|
||||
- 🩹(frontend) enable screen share button in PiP #1458
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(summary) generalized stt api call #1420
|
||||
- ♻️(env) refactor env variables handling
|
||||
- 🚸(frontend) use "Advanced" instead of "Premium" in the sidepanel
|
||||
|
||||
## [1.21.0] - 2026-06-15
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) allow disabling silent login via a URL parameter
|
||||
- ✨(frontend) allow hiding the login button via a URL parameter
|
||||
- ✨(summary) add optional satisfaction survey footer
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(frontend) enhance noise reduction with BBBA audio processing pipeline
|
||||
- 🚸(frontend) mute join notification sound in larger rooms
|
||||
- 🚸(frontend) mute participants by default when joining a large meeting
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix metadata agent collector enabled check
|
||||
|
||||
### Fixed
|
||||
|
||||
- ♿️(frontend) improve accessibilty of the Effects panel #1401
|
||||
|
||||
## [1.20.0] - 2026-06-12
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(addon) improve Outlook add-on: i18n support, feedback link, smarter link
|
||||
- ⬆️(frontend) upgrade react-i18next from 15.1.1 to 17.0.8
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix noise reduction left-channel-only audio
|
||||
|
||||
## [1.19.0] - 2026-06-04
|
||||
|
||||
### Added
|
||||
@@ -25,7 +73,7 @@ and this project adheres to
|
||||
|
||||
- 🔇(summary) make ffmpeg quiet #1404
|
||||
- 🔒️(backend) prevent accessing files if they are not ready #1395
|
||||
- ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
|
||||
- # ⬆️(backend) upgrade idna to >=3.15 to address CVE-2026-45409
|
||||
|
||||
## [1.18.0] - 2026-06-03
|
||||
|
||||
|
||||
@@ -75,7 +75,8 @@ create-env-files: \
|
||||
env.d/development/kc_postgresql \
|
||||
env.d/development/summary \
|
||||
env.d/development/kube-secret \
|
||||
env.d/development/multi_user_transcriber
|
||||
env.d/development/multi_user_transcriber \
|
||||
env.d/development/metadata_collector
|
||||
.PHONY: create-env-files
|
||||
|
||||
bootstrap: ## Prepare Docker images for the project
|
||||
@@ -292,6 +293,9 @@ env.d/development/kube-secret:
|
||||
env.d/development/multi_user_transcriber:
|
||||
cp -n env.d/development/multi_user_transcriber.dist env.d/development/multi_user_transcriber
|
||||
|
||||
env.d/development/metadata_collector:
|
||||
cp -n env.d/development/metadata_collector.dist env.d/development/metadata_collector
|
||||
|
||||
# -- Internationalization
|
||||
|
||||
env.d/development/crowdin:
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/issues-closed/suitenumerique/meet"/>
|
||||
<a href="https://github.com/suitenumerique/meet/blob/main/LICENSE">
|
||||
<img alt="GitHub closed issues" src="https://img.shields.io/github/license/suitenumerique/meet"/>
|
||||
</a>
|
||||
</a>
|
||||
<a href="https://digitalpublicgoods.net/r/la-suite-meet-simple-video-conferencing">
|
||||
<img src="https://img.shields.io/badge/Verified-DPG-3333AB?logo=data:image/svg%2bxml;base64,PHN2ZyB3aWR0aD0iMzEiIGhlaWdodD0iMzMiIHZpZXdCb3g9IjAgMCAzMSAzMyIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTE0LjIwMDggMjEuMzY3OEwxMC4xNzM2IDE4LjAxMjRMMTEuNTIxOSAxNi40MDAzTDEzLjk5MjggMTguNDU5TDE5LjYyNjkgMTIuMjExMUwyMS4xOTA5IDEzLjYxNkwxNC4yMDA4IDIxLjM2NzhaTTI0LjYyNDEgOS4zNTEyN0wyNC44MDcxIDMuMDcyOTdMMTguODgxIDUuMTg2NjJMMTUuMzMxNCAtMi4zMzA4MmUtMDVMMTEuNzgyMSA1LjE4NjYyTDUuODU2MDEgMy4wNzI5N0w2LjAzOTA2IDkuMzUxMjdMMCAxMS4xMTc3TDMuODQ1MjEgMTYuMDg5NUwwIDIxLjA2MTJMNi4wMzkwNiAyMi44Mjc3TDUuODU2MDEgMjkuMTA2TDExLjc4MjEgMjYuOTkyM0wxNS4zMzE0IDMyLjE3OUwxOC44ODEgMjYuOTkyM0wyNC44MDcxIDI5LjEwNkwyNC42MjQxIDIyLjgyNzdMMzAuNjYzMSAyMS4wNjEyTDI2LjgxNzYgMTYuMDg5NUwzMC42NjMxIDExLjExNzdMMjQuNjI0MSA5LjM1MTI3WiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+Cg==" alt="DPG Badge"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -28,6 +31,14 @@
|
||||
## La Suite Meet: Simple Video Conferencing
|
||||
|
||||
Powered by [LiveKit](https://livekit.io/), La Suite Meet offers Zoom-level performance with high-quality video and audio. No installation required—simply join calls directly from your browser. Check out LiveKit's impressive optimizations in their [blog post](https://blog.livekit.io/livekit-one-dot-zero/).
|
||||
|
||||
> [!TIP]
|
||||
> New here? Start by introducing yourself in our Matrix channel:
|
||||
> **https://matrix.to/#/#meet-official:matrix.org**
|
||||
>
|
||||
> We’re happy to discuss ideas, answer questions, and help to deploy LaSuite Meet.
|
||||
|
||||
|
||||
### Features
|
||||
- Optimized for stability in large meetings (+100 p.)
|
||||
- Support for multiple screen sharing streams
|
||||
@@ -52,7 +63,7 @@ We’re continuously adding new features to enhance your experience, with the la
|
||||
|
||||
### 🚀 Major roll out to all French public servants
|
||||
|
||||
On the 25th of January 2026, David Amiel, France’s Minister for Civil Service and State Reform, announced the full deployment of Visio—the French government’s dedicated Meet platform—to all public servants. ([Source in French](https://www.latribune.fr/article/la-tribune-dimanche/politique/73157688099661/david-amiel-ministre-delegue-de-la-fonction-publique-nous-allons-sortir-de-la-dependance-aux-outils-americains))
|
||||
On the 29th of January 2026, Prime Minister Sébastien Lecornu, announced the full deployment of Visio—the French government’s dedicated Meet platform—to all public servants. ([Source in English](https://www.nytimes.com/2026/01/29/world/europe/france-zoom-alternative-visio.html))
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -84,22 +95,57 @@ We use Kubernetes for our [production instance](https://visio.numerique.gouv.fr/
|
||||
#### Known instances
|
||||
We hope to see many more, here is an incomplete list of public La Suite Meet instances. Feel free to make a PR to add ones that are not listed below🙏
|
||||
|
||||
| Url | Org | Access |
|
||||
|---------------------------------------------------------------| --- | ------- |
|
||||
| [visio.numerique.gouv.fr](https://visio.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up|
|
||||
| [visio.suite.anct.gouv.fr](https://visio.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up|
|
||||
| [visio.lasuite.coop](https://visio.lasuite.coop/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
|
||||
| [mosacloud.cloud](https://mosa.cloud/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
|
||||
| Url | Org | Access |
|
||||
|---------------------------------------------------------------|--------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [visio.numerique.gouv.fr](https://visio.numerique.gouv.fr/) | DINUM | French public agents working for the central administration and the extended public sphere. ProConnect is required to login in or sign up |
|
||||
| [visio.suite.anct.gouv.fr](https://visio.suite.anct.gouv.fr/) | ANCT | French public agents working for the territorial administration and the extended public sphere. ProConnect is required to login in or sign up |
|
||||
| [visio.lasuite.coop](https://visio.lasuite.coop/) | lasuite.coop | Free and open demo to all. Content and accounts are reset after one month |
|
||||
| [mosacloud.cloud](https://mosa.cloud/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
|
||||
| [Clever Cloud](https://www.clever.cloud/product/visio/) | clever cloud | Openvisio is a sovereign video conferencing solution based on LaSuite Meet offered by [Clever Cloud](https://www.clever.cloud/). |
|
||||
| [Email.eu](https://email.eu/) | Email.eu | Sovereign business workspace. |
|
||||
|
||||
|
||||
## Contributing
|
||||
# Contributing
|
||||
|
||||
We <3 contributions of any kind, big and small:
|
||||
We <3 contributions of all kinds **big or small** and we’re genuinely glad you’re here. 🌱
|
||||
|
||||
- Vote on features or get early access to beta functionality in our [roadmap](https://github.com/orgs/suitenumerique/projects/11/views/4)
|
||||
- Open a PR (see our instructions on [developing La Suite Meet locally](https://github.com/suitenumerique/meet/blob/main/docs/developping_locally.md))
|
||||
- Submit a [feature request](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=enhancement&template=Feature_request.md) or [bug report](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md)
|
||||
### Start by saying hi
|
||||
|
||||
**The best first contribution is simply to come say hi.**
|
||||
|
||||
Before opening a PR, especially a larger one, or one written with the help of AI, we encourage you to reach out to a maintainer on our [Matrix channel](https://matrix.to/#/#meet-official:matrix.org) (@antoine.lebaud:matrix.org).
|
||||
|
||||
Getting in touch early helps us align on goals, avoid duplicated or wasted effort, and build a community that stays active, welcoming, and fun to be part of. There are no silly questions here: whether you’ve shipped hundreds of PRs or you’re just getting started, you’re welcome.
|
||||
|
||||
### AI contributions
|
||||
|
||||
AI-assisted contributions are welcome. But code is never the end goal. What matters most is building relationships, sharing knowledge, and growing a sustainable community over time.
|
||||
|
||||
If your contribution has been heavily generated with AI, please be transparent about it. This helps maintainers review it with the right context and respects the time they invest in the project.
|
||||
|
||||
Using AI does not transfer ownership of the contribution: you should still fully understand the code, the problem it solves, and the reasoning behind the approach you propose. In short, even if AI helped write it, the why should still be yours.
|
||||
|
||||
### Contributions beyond code
|
||||
|
||||
**Not technical? We need you too.**
|
||||
Open source is much more than code. Writing documentation, improving onboarding, translating content, answering questions, reporting bugs, or simply helping others feel welcome all make a huge difference.
|
||||
|
||||
### Ways to contribute
|
||||
|
||||
When you’re ready, here are a few ways to get involved:
|
||||
|
||||
* 👋 **Say hello** and share your ideas with the community and maintainers on our [Matrix channel](https://matrix.to/#/#meet-official:matrix.org)
|
||||
* 🛠️ **Open a PR** by following our guide to [develop La Suite Meet locally](https://github.com/suitenumerique/meet/blob/main/docs/developping_locally.md)
|
||||
* 💡 **Suggest an idea** by opening a [feature request](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=enhancement&template=Feature_request.md)
|
||||
* 🐛 **Report a bug** by opening a [bug report](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md)
|
||||
|
||||
Thank you for helping build something open, useful, and human. 💙
|
||||
|
||||
### Community call
|
||||
|
||||
We host a community call on the first Friday of every month to share updates, discuss ideas, and connect with contributors.
|
||||
|
||||
Whether you’re actively contributing or just curious about the project, you’re welcome to join. More details are shared on the [Matrix channel](https://matrix.to/#/#meet-official:matrix.org).
|
||||
|
||||
## Philosophy
|
||||
|
||||
|
||||
+11
-18
@@ -93,7 +93,7 @@ services:
|
||||
networks:
|
||||
- resource-server
|
||||
- default
|
||||
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: meet:backend-development
|
||||
@@ -237,29 +237,22 @@ services:
|
||||
- livekit-egress
|
||||
|
||||
livekit-egress:
|
||||
image: livekit/egress:v1.11.0
|
||||
environment:
|
||||
EGRESS_CONFIG_FILE: ./livekit-egress.yaml
|
||||
volumes:
|
||||
- ./docker/livekit/config/livekit-egress.yaml:/livekit-egress.yaml
|
||||
- ./docker/livekit/out:/out
|
||||
depends_on:
|
||||
- redis
|
||||
image: livekit/egress:v1.11.0
|
||||
environment:
|
||||
EGRESS_CONFIG_FILE: ./livekit-egress.yaml
|
||||
volumes:
|
||||
- ./docker/livekit/config/livekit-egress.yaml:/livekit-egress.yaml
|
||||
- ./docker/livekit/out:/out
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
metadata-collector-dev:
|
||||
build:
|
||||
context: ./src/agents
|
||||
target: development
|
||||
command: ["python", "metadata_collector.py", "dev"]
|
||||
environment:
|
||||
- LIVEKIT_URL=ws://livekit:7880
|
||||
- LIVEKIT_API_KEY=devkey
|
||||
- LIVEKIT_API_SECRET=secret
|
||||
- AWS_S3_ENDPOINT_URL=minio:9000
|
||||
- AWS_S3_ACCESS_KEY_ID=meet
|
||||
- AWS_S3_SECRET_ACCESS_KEY=password
|
||||
- AWS_STORAGE_BUCKET_NAME=meet-media-storage
|
||||
- AWS_S3_SECURE_ACCESS=False
|
||||
env_file:
|
||||
- env.d/development/metadata_collector
|
||||
volumes:
|
||||
- ./src/agents:/app
|
||||
- /app/.venv
|
||||
|
||||
@@ -60,6 +60,8 @@ USER root
|
||||
|
||||
# Security patches for known CVEs
|
||||
RUN apk update && apk upgrade \
|
||||
libcrypto3>=3.5.7-r0 \
|
||||
libssl3>=3.5.7-r0 \
|
||||
musl \
|
||||
musl-utils \
|
||||
zlib>=1.3.2-r0 \
|
||||
|
||||
@@ -48,9 +48,12 @@ server {
|
||||
|
||||
set $nonce $request_id;
|
||||
|
||||
set $csp "upgrade-insecure-requests; ";
|
||||
set $csp "default-src 'self'; upgrade-insecure-requests; ";
|
||||
set $csp "${csp}frame-ancestors ${ms_domains}; ";
|
||||
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
|
||||
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
|
||||
set $csp "${csp}img-src 'self' data:; ";
|
||||
set $csp "${csp}font-src 'self' data:; ";
|
||||
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
|
||||
set $csp "${csp}frame-src 'none'; ";
|
||||
set $csp "${csp}object-src 'none'; ";
|
||||
|
||||
@@ -96,7 +96,7 @@ sequenceDiagram
|
||||
| **RECORDING_WORKER_CLASSES** | Dict | `{ "screen_recording": "core.recording.worker.services.VideoCompositeEgressService", "transcript": "core.recording.worker.services.AudioCompositeEgressService" }` | Maps recording types to their worker service classes. |
|
||||
| **RECORDING_EVENT_PARSER_CLASS** | String | `"core.recording.event.parsers.MinioParser"` | Class responsible for parsing storage events and updating the backend. |
|
||||
| **RECORDING_ENABLE_STORAGE_EVENT_AUTH** | Boolean | `True` | Enable authentication for storage event webhook requests. |
|
||||
| **RECORDING_STORAGE_EVENT_ENABLE** | Boolean | `False` | Enable handling of storage events (must configure webhook in storage). |
|
||||
| **RECORDING_STORAGE_EVENT_ENABLE** | Boolean | `False` | Enable handling of storage events (must configure webhook in storage). If `False`, fallback to LiveKit egress complete webhook. |
|
||||
| **RECORDING_STORAGE_EVENT_TOKEN** | Secret/File | `None` | Token used to authenticate storage webhook requests, if `RECORDING_ENABLE_STORAGE_EVENT_AUTH` is enabled. |
|
||||
| **RECORDING_EXPIRATION_DAYS** | Integer | `None` | Number of days before recordings expire. Should match bucket lifecycle policy. Set to `None` for no expiration. |
|
||||
| **RECORDING_MAX_DURATION** | Integer | `None` | Maximum duration of a recording in milliseconds. Must be synced with the LiveKit Egress configuration. Set to None for unlimited duration. When the maximum duration is reached, the recording is automatically stopped and saved, and the user is prompted in the frontend with an alert message. |
|
||||
|
||||
@@ -80,7 +80,6 @@ sequenceDiagram
|
||||
| whisperx_api_key | Secret | — | API key for accessing WhisperX. |
|
||||
| whisperx_base_url | String | `"https://api.whisperx.com/v1"` | Base URL for the WhisperX API. |
|
||||
| whisperx_asr_model | String | `"whisper-1"` | ASR model used for transcription. |
|
||||
| whisperx_max_retries | Integer | `0` | Maximum number of retries for WhisperX API requests. |
|
||||
| webhook_max_retries | Integer | `2` | Maximum retries for webhook requests. |
|
||||
| webhook_status_forcelist | List[Int] | `[502, 503, 504]` | HTTP status codes triggering webhook retry. |
|
||||
| webhook_backoff_factor | Float | `0.1` | Exponential backoff factor for webhook retries. |
|
||||
|
||||
@@ -244,6 +244,69 @@ meet-admin <none> meet.127.0.0.1.nip.io localhost 80, 44
|
||||
|
||||
You can use LaSuite Meet on https://meet.127.0.0.1.nip.io from the local device. The provisioning user in keycloak is meet/meet.
|
||||
|
||||
## Rebranding the favicon
|
||||
|
||||
The favicon is bundled into the frontend image and served as a set of static
|
||||
files from `/usr/share/nginx/html` (`favicon.ico`, `favicon-16x16.png`,
|
||||
`favicon-32x32.png`, `apple-touch-icon.png`, `android-chrome-192x192.png`,
|
||||
`android-chrome-512x512.png`, `icon.png`). To rebrand without forking and
|
||||
rebuilding the image, overlay your own icons onto those paths with a volume —
|
||||
this serves the right icon from the first byte (no rebuild, no flash) and
|
||||
covers every variant, including the iOS home-screen and Android/PWA icons.
|
||||
|
||||
Put your icons in a `ConfigMap` (`binaryData` keeps the PNGs intact)…
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: meet-favicon
|
||||
binaryData:
|
||||
# base64 of each replacement icon
|
||||
favicon.ico: <base64…>
|
||||
favicon-16x16.png: <base64…>
|
||||
favicon-32x32.png: <base64…>
|
||||
apple-touch-icon.png: <base64…>
|
||||
android-chrome-192x192.png: <base64…>
|
||||
android-chrome-512x512.png: <base64…>
|
||||
```
|
||||
|
||||
```bash
|
||||
# e.g. build the ConfigMap straight from a directory of icons
|
||||
$ kubectl create configmap meet-favicon --from-file=./my-icons/
|
||||
```
|
||||
|
||||
…then mount each file over the bundled one via the chart's
|
||||
`frontend.extraVolumes` / `frontend.extraVolumeMounts` (the `subPath` mounts
|
||||
the single file without hiding the rest of `html/`):
|
||||
|
||||
```yaml
|
||||
frontend:
|
||||
extraVolumes:
|
||||
- name: favicon
|
||||
configMap:
|
||||
name: meet-favicon
|
||||
extraVolumeMounts:
|
||||
- name: favicon
|
||||
mountPath: /usr/share/nginx/html/favicon.ico
|
||||
subPath: favicon.ico
|
||||
- name: favicon
|
||||
mountPath: /usr/share/nginx/html/favicon-16x16.png
|
||||
subPath: favicon-16x16.png
|
||||
- name: favicon
|
||||
mountPath: /usr/share/nginx/html/favicon-32x32.png
|
||||
subPath: favicon-32x32.png
|
||||
- name: favicon
|
||||
mountPath: /usr/share/nginx/html/apple-touch-icon.png
|
||||
subPath: apple-touch-icon.png
|
||||
- name: favicon
|
||||
mountPath: /usr/share/nginx/html/android-chrome-192x192.png
|
||||
subPath: android-chrome-192x192.png
|
||||
- name: favicon
|
||||
mountPath: /usr/share/nginx/html/android-chrome-512x512.png
|
||||
subPath: android-chrome-512x512.png
|
||||
```
|
||||
|
||||
## All options
|
||||
|
||||
These are the environmental options available on meet backend.
|
||||
@@ -344,7 +407,7 @@ These are the environmental options available on meet backend.
|
||||
| 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_ENABLE | Enable recording storage events. If false, fallback to egress webhook. | false |
|
||||
| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | |
|
||||
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
|
||||
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
|
||||
|
||||
@@ -57,6 +57,7 @@ OIDC_RS_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
LIVEKIT_API_SECRET=secret
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_URL=http://127.0.0.1.nip.io:7880
|
||||
LIVEKIT_INTERNAL_URL=http://livekit:7880
|
||||
LIVEKIT_VERIFY_SSL=False
|
||||
ALLOW_UNREGISTERED_ROOMS=False
|
||||
|
||||
@@ -86,6 +87,9 @@ ROOM_TELEPHONY_ENABLED=True
|
||||
# Metadata
|
||||
METADATA_COLLECTOR_ENABLED=True
|
||||
|
||||
# Subtitle
|
||||
ROOM_SUBTITLE_ENABLED=False
|
||||
|
||||
FRONTEND_USE_FRENCH_GOV_FOOTER=False
|
||||
FRONTEND_USE_PROCONNECT_BUTTON=False
|
||||
|
||||
@@ -94,3 +98,4 @@ EXTERNAL_API_ENABLED=True
|
||||
APPLICATION_JWT_AUDIENCE=http://localhost:8071/external-api/v1.0/
|
||||
APPLICATION_JWT_SECRET_KEY=devKey
|
||||
APPLICATION_BASE_URL=http://localhost:3000
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
LIVEKIT_URL=ws://livekit:7880
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_SECRET=secret
|
||||
|
||||
AWS_S3_ENDPOINT_URL=minio:9000
|
||||
AWS_S3_ACCESS_KEY_ID=meet
|
||||
AWS_S3_SECRET_ACCESS_KEY=password
|
||||
AWS_STORAGE_BUCKET_NAME=meet-media-storage
|
||||
AWS_S3_SECURE_ACCESS=False
|
||||
@@ -2,8 +2,10 @@ LIVEKIT_URL=ws://livekit:7880
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_SECRET=secret
|
||||
|
||||
STT_PROVIDER=kyutai
|
||||
STT_PROVIDER=kyutai # kyutai, deepgram
|
||||
ENABLE_SILERO_VAD=False
|
||||
|
||||
DEEPGRAM_API_KEY=
|
||||
|
||||
KYUTAI_STT_BASE_URL=
|
||||
KYUTAI_API_KEY=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
APP_NAME="meet-app-summary-dev"
|
||||
APP_API_TOKEN="password"
|
||||
|
||||
AWS_STORAGE_BUCKET_NAME="http://meet-media-storage"
|
||||
AWS_STORAGE_BUCKET_NAME="meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL="minio:9000"
|
||||
AWS_S3_SECURE_ACCESS=false
|
||||
|
||||
@@ -20,5 +20,13 @@ LLM_MODEL="albert-large"
|
||||
WEBHOOK_API_TOKEN="secret"
|
||||
WEBHOOK_URL="https://configure-your-url.com"
|
||||
|
||||
IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED=true
|
||||
RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP=0.5
|
||||
RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS=true
|
||||
RESOLVE_SPEAKER_MAX_WORD_DURATION=1
|
||||
|
||||
POSTHOG_API_KEY="your-posthog-key"
|
||||
POSTHOG_ENABLED="False"
|
||||
|
||||
# Transcription
|
||||
TRANSCRIPTION_SATISFACTION_FORM_BASE_URL=
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
|
||||
<Id>a025f0f6-757a-4790-97f3-99c66c4a5795</Id>
|
||||
<Version>0.0.1.0</Version>
|
||||
<Version>0.0.2.0</Version>
|
||||
<ProviderName>__APP_NAME__</ProviderName>
|
||||
<DefaultLocale>fr-FR</DefaultLocale>
|
||||
<DisplayName DefaultValue="__APP_NAME__"/>
|
||||
@@ -87,9 +87,9 @@
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Add.16x16"/>
|
||||
<bt:Image size="32" resid="Add.32x32"/>
|
||||
<bt:Image size="80" resid="Add.80x80"/>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromMail</FunctionName>
|
||||
@@ -126,9 +126,9 @@
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Add.16x16"/>
|
||||
<bt:Image size="32" resid="Add.32x32"/>
|
||||
<bt:Image size="80" resid="Add.80x80"/>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
|
||||
@@ -175,15 +175,34 @@
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<!-- Default (French) -->
|
||||
<bt:String id="GroupLabel" DefaultValue="__APP_NAME__"/>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__"/>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien __APP_NAME__">
|
||||
<bt:Override Locale="en-US" Value="Add a __APP_NAME__ link"/>
|
||||
<bt:Override Locale="de-DE" Value="__APP_NAME__-Link hinzufügen"/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
|
||||
<bt:Override Locale="en-US" Value="Open settings"/>
|
||||
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
|
||||
</bt:String>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
|
||||
<bt:Override Locale="en-US" Value="Settings"/>
|
||||
<bt:Override Locale="de-DE" Value="Einstellungen"/>
|
||||
</bt:String>
|
||||
</bt:ShortStrings>
|
||||
<bt:LongStrings>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__."/>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement."/>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__."/>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion __APP_NAME__ et l'insère dans l'événement.">
|
||||
<bt:Override Locale="de-DE" Value="Generiert einen __APP_NAME__-Besprechungslink und fügt ihn in den Termin ein."/>
|
||||
<bt:Override Locale="en-US" Value="Generates a __APP_NAME__ meeting link and inserts it into the item."/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
|
||||
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
|
||||
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
|
||||
</bt:String>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion __APP_NAME__.">
|
||||
<bt:Override Locale="de-DE" Value="Öffnet die __APP_NAME__-Verbindungseinstellungen."/>
|
||||
<bt:Override Locale="en-US" Value="Opens the __APP_NAME__ connection settings."/>
|
||||
</bt:String>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
|
||||
Generated
+65
-28
@@ -9,33 +9,35 @@
|
||||
"version": "0.0.1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"core-js": "^3.49.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
"core-js": "3.49.0",
|
||||
"i18next": "26.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@types/office-js": "^1.0.377",
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-inject-attributes-plugin": "^1.0.6",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@types/office-js": "1.0.582",
|
||||
"@types/office-runtime": "1.0.36",
|
||||
"acorn": "8.16.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"eslint-plugin-office-addins": "4.0.6",
|
||||
"file-loader": "6.2.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-inject-attributes-plugin": "1.0.6",
|
||||
"html-webpack-plugin": "5.6.6",
|
||||
"office-addin-cli": "2.0.6",
|
||||
"office-addin-debugging": "6.0.6",
|
||||
"office-addin-dev-certs": "2.0.6",
|
||||
"office-addin-lint": "3.0.6",
|
||||
"office-addin-manifest": "2.1.2",
|
||||
"office-addin-prettier-config": "2.0.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"process": "0.11.10",
|
||||
"source-map-loader": "5.0.0",
|
||||
"webpack": "5.105.4",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.2.4"
|
||||
}
|
||||
},
|
||||
@@ -2111,9 +2113,7 @@
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
@@ -9363,6 +9363,43 @@
|
||||
"node": ">=10.18"
|
||||
}
|
||||
},
|
||||
"node_modules/i18next": {
|
||||
"version": "26.3.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.1.tgz",
|
||||
"integrity": "sha512-txQqd5EULsqEh9OJqRH15aCaOuy/nLJyhw5EHCSKLKJE1aBbb3Zve2+uQIxgWhPm1QqUQoWyQBm2kfmmIrzkcQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com/i18next"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||
},
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://www.locize.com"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/i18next-browser-languagedetector": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/i18next-browser-languagedetector/-/i18next-browser-languagedetector-8.2.1.tgz",
|
||||
"integrity": "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.23.2"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -15213,7 +15250,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
||||
@@ -26,33 +26,35 @@
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "^3.49.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
"core-js": "3.49.0",
|
||||
"i18next": "26.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.24.0",
|
||||
"@babel/preset-env": "^7.25.4",
|
||||
"@types/office-js": "^1.0.377",
|
||||
"@types/office-runtime": "^1.0.35",
|
||||
"acorn": "^8.11.3",
|
||||
"babel-loader": "^9.1.3",
|
||||
"copy-webpack-plugin": "^14.0.0",
|
||||
"eslint-plugin-office-addins": "^4.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-loader": "^5.0.0",
|
||||
"html-webpack-inject-attributes-plugin": "^1.0.6",
|
||||
"html-webpack-plugin": "^5.6.0",
|
||||
"office-addin-cli": "^2.0.3",
|
||||
"office-addin-debugging": "^6.0.3",
|
||||
"office-addin-dev-certs": "^2.0.3",
|
||||
"office-addin-lint": "^3.0.3",
|
||||
"office-addin-manifest": "^2.0.3",
|
||||
"office-addin-prettier-config": "^2.0.1",
|
||||
"os-browserify": "^0.3.0",
|
||||
"process": "^0.11.10",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"webpack": "^5.95.0",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@types/office-js": "1.0.582",
|
||||
"@types/office-runtime": "1.0.36",
|
||||
"acorn": "8.16.0",
|
||||
"babel-loader": "9.2.1",
|
||||
"copy-webpack-plugin": "14.0.0",
|
||||
"eslint-plugin-office-addins": "4.0.6",
|
||||
"file-loader": "6.2.0",
|
||||
"html-loader": "5.1.0",
|
||||
"html-webpack-inject-attributes-plugin": "1.0.6",
|
||||
"html-webpack-plugin": "5.6.6",
|
||||
"office-addin-cli": "2.0.6",
|
||||
"office-addin-debugging": "6.0.6",
|
||||
"office-addin-dev-certs": "2.0.6",
|
||||
"office-addin-lint": "3.0.6",
|
||||
"office-addin-manifest": "2.1.2",
|
||||
"office-addin-prettier-config": "2.0.1",
|
||||
"os-browserify": "0.3.0",
|
||||
"process": "0.11.10",
|
||||
"source-map-loader": "5.0.0",
|
||||
"webpack": "5.105.4",
|
||||
"webpack-cli": "5.1.4",
|
||||
"webpack-dev-server": "5.2.4"
|
||||
},
|
||||
"prettier": "office-addin-prettier-config",
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
/* global Office */
|
||||
const { APP_NAME } = require("../common/index");
|
||||
const { createRoom, initSession } = require("../common/api");
|
||||
const { startPolling } = require("../common/polling");
|
||||
const { saveSession, loadSession } = require("../common/session");
|
||||
const { openTransitDialog } = require("../common/transitDialog");
|
||||
const { buildMeetingMessage } = require("../common/messageBuilder");
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { initI18n, t } = require("../common/i18n");
|
||||
const { isMeetingAlreadyAdded } = require("../common/meetingDetector");
|
||||
|
||||
Office.onReady(async function (info) {
|
||||
|
||||
await initI18n()
|
||||
|
||||
Office.onReady(function (info) {
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
}
|
||||
@@ -22,41 +28,52 @@ function notify(message) {
|
||||
}
|
||||
|
||||
function insertMeetingLink(event, session) {
|
||||
const item = Office.context.mailbox.item;
|
||||
|
||||
isMeetingAlreadyAdded(item)
|
||||
.then((alreadyAdded) => {
|
||||
if (alreadyAdded) {
|
||||
notify(t("meeting.already_added", { app_name: APP_NAME }));
|
||||
event.completed();
|
||||
return;
|
||||
}
|
||||
return _doInsertMeetingLink(event, session);
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(t("meeting.error.details", { message: err.message }));
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
function _doInsertMeetingLink(event, session) {
|
||||
createRoom(session)
|
||||
.then((data) => {
|
||||
const { url, message } = buildMeetingMessage(data);
|
||||
const isWeb = Office.context.diagnostics.platform === "OfficeOnline";
|
||||
const { url, text } = buildMeetingMessage(data, isWeb);
|
||||
const item = Office.context.mailbox.item;
|
||||
const coercionType = isWeb ? Office.CoercionType.Html : Office.CoercionType.Text;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
|
||||
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(`Erreur de lecture : ${getResult.error.message}`);
|
||||
item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(t("meeting.error.details", { message: setResult.error.message }));
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const newBody = getResult.value + message;
|
||||
item.body.setAsync(newBody, { coercionType: Office.CoercionType.Html }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(`Erreur d'insertion : ${setResult.error.message}`);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
notify(t("meeting.link_inserted"));
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
notify("Lien de réunion inséré !");
|
||||
resolve();
|
||||
return;
|
||||
item.location.setAsync(url, (locationResult) => {
|
||||
if (locationResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(t("meeting.error.details", { message: locationResult.error.message }));
|
||||
} else {
|
||||
notify(t("meeting.link_inserted"));
|
||||
}
|
||||
|
||||
item.location.setAsync(url, (locationResult) => {
|
||||
if (locationResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(`Erreur de localisation : ${locationResult.error.message}`);
|
||||
} else {
|
||||
notify("Lien de réunion inséré !");
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -79,11 +96,11 @@ function connect(event) {
|
||||
});
|
||||
},
|
||||
onTimeout: () => {
|
||||
notify("Connexion expirée, veuillez réessayer.");
|
||||
notify(t("meeting.error.auth"));
|
||||
event.completed();
|
||||
},
|
||||
onError: (err) => {
|
||||
notify("Une erreur est survenue, veuillez ré-essayer");
|
||||
notify(t("meeting.error.retry"));
|
||||
event.completed();
|
||||
},
|
||||
});
|
||||
@@ -99,7 +116,7 @@ function connect(event) {
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(`Erreur : ${err.message}`);
|
||||
notify(t("meeting.error.details", { message: err.message }));
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
const { APP_NAME } = require("../common");
|
||||
|
||||
const i18nextModule = require("i18next");
|
||||
const i18next = i18nextModule.default || i18nextModule;
|
||||
|
||||
const fr = require("../locales/fr/translation.json");
|
||||
const en = require("../locales/en/translation.json");
|
||||
const de = require("../locales/de/translation.json");
|
||||
|
||||
async function initI18n() {
|
||||
const lng = typeof Office !== "undefined" ? Office.context.displayLanguage : navigator.language;
|
||||
|
||||
await i18next.init({
|
||||
lng,
|
||||
fallbackLng: "fr",
|
||||
interpolation: { escapeValue: false },
|
||||
resources: {
|
||||
fr: { translation: fr },
|
||||
en: { translation: en },
|
||||
de: { translation: de },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function t(key, vars) {
|
||||
return i18next.t(key, vars);
|
||||
}
|
||||
|
||||
function translateUI() {
|
||||
document.querySelectorAll("[data-i18n]").forEach((el) => {
|
||||
const key = el.getAttribute("data-i18n");
|
||||
el.textContent = t(key, { app_name: APP_NAME });
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-attr]").forEach((el) => {
|
||||
const pairs = el.getAttribute("data-i18n-attr").split(",");
|
||||
pairs.forEach((pair) => {
|
||||
const [attr, key] = pair.split(":");
|
||||
el.setAttribute(attr, t(key, { app_name: APP_NAME }));
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-i18n-aria]").forEach((el) => {
|
||||
el.setAttribute("aria-label", t(el.getAttribute("data-i18n-aria")));
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { initI18n, t, translateUI };
|
||||
@@ -1,7 +1,11 @@
|
||||
const BASE_URL = window.__APP_CONFIG__?.BASE_URL || "https://meet.127.0.0.1.nip.io";
|
||||
const APP_NAME = window.__APP_CONFIG__?.APP_NAME || "LaSuite Meet";
|
||||
const ENABLE_SOURCE_TRACKING = window.__APP_CONFIG__?.ENABLE_SOURCE_TRACKING === "true";
|
||||
const FEEDBACK_FORM = window.__APP_CONFIG__?.FEEDBACK_FORM || null;
|
||||
|
||||
module.exports = {
|
||||
BASE_URL,
|
||||
APP_NAME,
|
||||
ENABLE_SOURCE_TRACKING,
|
||||
FEEDBACK_FORM
|
||||
};
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
const { BASE_URL } = require("./index");
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves to true if a meeting link is already present
|
||||
*/
|
||||
function isMeetingAlreadyAdded(item) {
|
||||
return Promise.all([_checkBody(item), _checkLocation(item)]).then(
|
||||
([inBody, inLocation]) => inBody || inLocation
|
||||
);
|
||||
}
|
||||
|
||||
function _checkBody(item) {
|
||||
return new Promise((resolve) => {
|
||||
item.body.getAsync(Office.CoercionType.Text, (result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
resolve(_containsMeetingUrl(result.value));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _checkLocation(item) {
|
||||
// Location only exists on appointments
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
item.location.getAsync((result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
resolve(_containsMeetingUrl(result.value));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _containsMeetingUrl(text) {
|
||||
if (!text) return false;
|
||||
return text.includes(BASE_URL);
|
||||
}
|
||||
|
||||
function removeMeetingLink(item) {
|
||||
return Promise.all([_removeFromBody(item), _removeFromLocation(item)]);
|
||||
}
|
||||
|
||||
function _removeFromBody(item) {
|
||||
return new Promise((resolve) => {
|
||||
item.body.getAsync(Office.CoercionType.Html, (result) => {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
const cleaned = _cleanBody(result.value || "");
|
||||
if (cleaned === null) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
item.body.setAsync(cleaned, { coercionType: Office.CoercionType.Html }, () => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function _removeFromLocation(item) {
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
item.location.getAsync((result) => {
|
||||
if (
|
||||
result.status === Office.AsyncResultStatus.Succeeded &&
|
||||
_containsMeetingUrl(result.value)
|
||||
) {
|
||||
item.location.setAsync("", () => resolve());
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const SEPARATOR = /─{10,}/;
|
||||
|
||||
/**
|
||||
* Returns cleaned HTML, or null if no meeting block was found.
|
||||
*/
|
||||
function _cleanBody(html) {
|
||||
const doc = new DOMParser().parseFromString(html, "text/html");
|
||||
|
||||
const hits = [];
|
||||
const walker = doc.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);
|
||||
while (walker.nextNode()) {
|
||||
if (SEPARATOR.test(walker.currentNode.nodeValue)) hits.push(walker.currentNode);
|
||||
}
|
||||
if (hits.length < 2) return null;
|
||||
|
||||
const range = doc.createRange();
|
||||
range.setStartBefore(hits[0]);
|
||||
range.setEndAfter(hits[hits.length - 1]);
|
||||
range.deleteContents();
|
||||
|
||||
return doc.documentElement.outerHTML;
|
||||
}
|
||||
|
||||
module.exports = { isMeetingAlreadyAdded, removeMeetingLink };
|
||||
@@ -1,4 +1,5 @@
|
||||
const { APP_NAME } = require("./index");
|
||||
const { APP_NAME, ENABLE_SOURCE_TRACKING } = require("./index");
|
||||
const { t } = require("./i18n");
|
||||
|
||||
function _formatPin(pin) {
|
||||
if (!pin) return "";
|
||||
@@ -20,33 +21,59 @@ function _formatPhone(phone) {
|
||||
return clean;
|
||||
}
|
||||
|
||||
function _appendTrackingParams(url) {
|
||||
if (!ENABLE_SOURCE_TRACKING) return url;
|
||||
const u = new URL(url);
|
||||
u.searchParams.set("from", "outlook-addin");
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
|
||||
// todo - escape html / link
|
||||
function buildMeetingMessage(data) {
|
||||
function buildMeetingMessage(data, isWeb) {
|
||||
if (!data?.url) {
|
||||
throw new Error("buildMeetingMessage: missing url in data");
|
||||
}
|
||||
|
||||
const url = data.url;
|
||||
const url = _appendTrackingParams(data.url);
|
||||
const phone = _formatPhone(data.telephony?.phone_number);
|
||||
const pin = _formatPin(data.telephony?.pin_code);
|
||||
|
||||
const telephonyBlock =
|
||||
phone && pin
|
||||
? `
|
||||
let textLines = "";
|
||||
let phoneLines = [];
|
||||
|
||||
Ou appelez (audio uniquement)
|
||||
(FR) ${phone}
|
||||
Code : ${pin}`
|
||||
: "";
|
||||
const join = t("meeting_message.join", { app_name: APP_NAME });
|
||||
const phoneOnly = t("meeting_message.phone_only");
|
||||
const phoneFr = t("meeting_message.phone_fr", { phone });
|
||||
const pinCode = t("meeting_message.pin_code", { pin });
|
||||
|
||||
const message = `<pre style="font-family:inherit; font-size:inherit; border:none; background:none; margin:16px 0;">
|
||||
────────────────────────────────────────
|
||||
Rejoindre la réunion ${APP_NAME}
|
||||
if (isWeb) {
|
||||
phoneLines = phone && pin ? [`<br><br>${phoneOnly}`, `<br>${phoneFr}`, `<br>${pinCode}`] : [];
|
||||
|
||||
<a href="${url}">${url}</a>${telephonyBlock}
|
||||
────────────────────────────────────────</pre>`;
|
||||
textLines = [
|
||||
"<br><br>────────────────────────────────────────",
|
||||
`<br>${join}`,
|
||||
`<br><br><a href="${url}" target="_blank">${url}</a>`,
|
||||
...phoneLines,
|
||||
"<br>────────────────────────────────────────<br>",
|
||||
];
|
||||
|
||||
return { url, message };
|
||||
} else {
|
||||
|
||||
phoneLines = phone && pin ? [`\n\n${phoneOnly}`, `\n${phoneFr}`, `\n${pinCode}`] : [];
|
||||
|
||||
textLines = [
|
||||
"\n\n────────────────────────────────────────",
|
||||
`\n${join}`,
|
||||
`\n\n${url}`,
|
||||
...phoneLines,
|
||||
"\n────────────────────────────────────────\n",
|
||||
];
|
||||
}
|
||||
|
||||
const text = textLines.join("");
|
||||
|
||||
return { url, text };
|
||||
}
|
||||
|
||||
module.exports = { buildMeetingMessage };
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"app": {
|
||||
"sideload": "Laden Sie das Add-In.",
|
||||
"loading": "Wird geladen..."
|
||||
},
|
||||
"unauth": {
|
||||
"intro": "Fügen Sie Ihren Outlook-Terminen ganz einfach einen {{app_name}}-Besprechungslink hinzu.",
|
||||
"proconnect_btn": "Aanmelden met ProConnect",
|
||||
"proconnect_link": "Wat is ProConnect?",
|
||||
"proconnect_link_title": "Wat is ProConnect? - nieuw venster"
|
||||
},
|
||||
"success": {
|
||||
"close_window": "Falls sich dieses Fenster nicht automatisch schließt, schließen Sie es bitte manuell."
|
||||
},
|
||||
"auth": {
|
||||
"disconnect": "Abmelden"
|
||||
},
|
||||
"meeting": {
|
||||
"already_added": "Es wurde bereits ein {{app_name}}-Meeting hinzugefügt.",
|
||||
"link_inserted": "Besprechungslink erfolgreich eingefügt",
|
||||
"generating": "Wird erstellt...",
|
||||
"add_meeting": "{{app_name}}-Besprechung hinzufügen",
|
||||
"remove_meeting": "{{app_name}}-Besprechung entfernen",
|
||||
"removing": "Wird entfernt...",
|
||||
"error": {
|
||||
"auth": "Ihre Sitzung ist abgelaufen. Bitte versuchen Sie es erneut.",
|
||||
"retry": "Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.",
|
||||
"details": "Fehler: {{message}}"
|
||||
}
|
||||
},
|
||||
"meeting_message": {
|
||||
"join": "An der {{app_name}}-Besprechung teilnehmen",
|
||||
"phone_only": "Oder per Telefon teilnehmen (nur Audio)",
|
||||
"phone_fr": "(FR) {{phone}}",
|
||||
"pin_code": "Code {{pin}}"
|
||||
},
|
||||
"footer": {
|
||||
"feedback": "Teilen Sie uns Ihr Feedback mit"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"app": {
|
||||
"sideload": "Please load the add-in.",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"unauth": {
|
||||
"intro": "Easily add a {{app_name}} meeting link to your Outlook events.",
|
||||
"proconnect_btn": "Sign in with ProConnect",
|
||||
"proconnect_link": "What is ProConnect?",
|
||||
"proconnect_link_title": "What is ProConnect? - new window"
|
||||
},
|
||||
"success": {
|
||||
"close_window": "If this window does not close automatically, please close it manually."
|
||||
},
|
||||
"auth": {
|
||||
"disconnect": "Sign out"
|
||||
},
|
||||
"meeting": {
|
||||
"already_added": "A {{app_name}} meeting has already been added.",
|
||||
"link_inserted": "Meeting link inserted successfully",
|
||||
"generating": "Generating...",
|
||||
"add_meeting": "Add a {{app_name}} meeting",
|
||||
"remove_meeting": "Remove the {{app_name}} meeting",
|
||||
"removing": "Removing...",
|
||||
"error": {
|
||||
"auth": "Your session has expired. Please try again.",
|
||||
"retry": "An error occurred. Please try again.",
|
||||
"details": "Error: {{message}}"
|
||||
}
|
||||
},
|
||||
"meeting_message": {
|
||||
"join": "Join the {{app_name}} meeting",
|
||||
"phone_only": "Or call in (audio only)",
|
||||
"phone_fr": "(FR) {{phone}}",
|
||||
"pin_code": "Code {{pin}}"
|
||||
},
|
||||
"footer": {
|
||||
"feedback": "Share your feedback"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"app": {
|
||||
"sideload": "Veuillez charger le complément.",
|
||||
"loading": "Chargement..."
|
||||
},
|
||||
"unauth": {
|
||||
"intro": "Ajoutez facilement un lien de réunion {{app_name}} à vos événements Outlook.",
|
||||
"proconnect_btn": "S'identifier avec ProConnect",
|
||||
"proconnect_link": "Qu'est-ce que ProConnect ?",
|
||||
"proconnect_link_title": "Qu'est-ce que ProConnect ? - nouvelle fenêtre"
|
||||
},
|
||||
"success": {
|
||||
"close_window": "Si cette fenêtre ne se ferme pas toute seule, veuillez la fermer manuellement."
|
||||
},
|
||||
"auth": {
|
||||
"disconnect": "Se déconnecter"
|
||||
},
|
||||
"meeting": {
|
||||
"already_added": "Une réunion {{app_name}} a déjà été ajoutée.",
|
||||
"link_inserted": "Lien de réunion inséré avec succès",
|
||||
"generating": "Génération...",
|
||||
"add_meeting": "Ajouter une réunion {{app_name}}",
|
||||
"remove_meeting": "Supprimer la réunion {{app_name}}",
|
||||
"removing": "Suppression en cours...",
|
||||
"error": {
|
||||
"auth": "Connexion expirée, veuillez réessayer.",
|
||||
"retry": "Une erreur est survenue, veuillez ré-essayer",
|
||||
"details": "Erreur : {{message}}"
|
||||
}
|
||||
},
|
||||
"meeting_message": {
|
||||
"join": "Rejoindre la réunion {{app_name}}",
|
||||
"phone_only": "Ou appelez (audio uniquement)",
|
||||
"phone_fr": "(FR) {{phone}}",
|
||||
"pin_code": "Code {{pin}}"
|
||||
},
|
||||
"footer": {
|
||||
"feedback": "Partagez-nous vos retours"
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,10 @@
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div class="spinner-container"
|
||||
role="progressbar"
|
||||
aria-label="Chargement..."
|
||||
data-i18n-aria="app.loading"
|
||||
>
|
||||
<svg class="spinner-svg"
|
||||
viewBox="0 0 28 28"
|
||||
@@ -40,5 +40,6 @@
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<p id="close-msg" style="display: none; text-align: center; font-size: 13px; color: #666; margin-top: 16px;" data-i18n="success.close_window"></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,20 +1,35 @@
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { exchangeSession } = require("../common/api");
|
||||
const { consume } = require("../common/transitToken");
|
||||
const { initI18n, translateUI } = require("../common/i18n");
|
||||
|
||||
applyAppName();
|
||||
(async () => {
|
||||
await initI18n();
|
||||
|
||||
const transitToken = consume();
|
||||
applyAppName();
|
||||
translateUI();
|
||||
|
||||
if (!transitToken) {
|
||||
console.error("Transit token not found in sessionStorage");
|
||||
window.close();
|
||||
} else {
|
||||
exchangeSession(transitToken)
|
||||
.catch((e) => {
|
||||
console.error(`Error occured: ${e}`);
|
||||
})
|
||||
.finally(() => {
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
const transitToken = consume();
|
||||
|
||||
if (!transitToken) {
|
||||
console.error("Transit token not found in sessionStorage");
|
||||
window.close();
|
||||
} else {
|
||||
exchangeSession(transitToken)
|
||||
.then(() => {
|
||||
document.querySelector(".spinner-container").style.display = "none";
|
||||
document.querySelector("#close-msg").style.display = "block";
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(`Error occured: ${e}`);
|
||||
})
|
||||
.finally(() => {
|
||||
// NOTE: doesn't work with the desktop client — the browser considers
|
||||
// this window wasn't opened by this script (it was opened externally),
|
||||
// so it blocks window.close() for security reasons. The "#close-msg"
|
||||
// shown above is the fallback for that case.g
|
||||
|
||||
window.close();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -115,15 +115,39 @@ button {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* ── Danger button (remove meeting) ── */
|
||||
|
||||
#btn-remove {
|
||||
background-color: #CA3632; /* error.400 */
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
}
|
||||
|
||||
#btn-remove:hover {
|
||||
background-color: #EE6A66; /* error.600 */
|
||||
}
|
||||
|
||||
#btn-remove:active {
|
||||
background-color: #F28D8A; /* error.700 */
|
||||
color: #F6AFAD; /* error.200 */
|
||||
}
|
||||
|
||||
#btn-remove:disabled {
|
||||
background-color: #F6AFAD; /* error.800 */
|
||||
color: #FAD2D1; /* error.900 */
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* ── Version ── */
|
||||
|
||||
#version-tag {
|
||||
position: fixed;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 11px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #6b7280;
|
||||
@@ -131,9 +155,29 @@ button {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#feedback-link {
|
||||
font-size: 11px;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #6b7280;
|
||||
text-decoration: underline;
|
||||
pointer-events: all; /* override parent's pointer-events: none */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#feedback-link:hover {
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
#footer-right {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.version-badge {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
background: #EEF1F4;
|
||||
color: #2845C1;
|
||||
padding: 1px 6px;
|
||||
border-radius: 3px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -10,47 +10,56 @@
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
|
||||
<div id="app-body">
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="view-loading">
|
||||
<p class="intro-text">Chargement...</p>
|
||||
<p class="intro-text" data-i18n="app.loading"></p>
|
||||
</div>
|
||||
|
||||
<!-- Unauthenticated -->
|
||||
<div id="view-unauth" style="display:none;">
|
||||
<p class="intro-text">
|
||||
<span>Ajoutez facilement un lien de réunion <span data-app-name></span> à vos événements Outlook.</span>
|
||||
<span data-i18n="unauth.intro"></span>
|
||||
</p>
|
||||
<hr class="divider" />
|
||||
<button class="proconnect-button" id="btn-connect">
|
||||
<span class="proconnect-sr-only">S'identifier avec ProConnect</span>
|
||||
<span class="proconnect-sr-only" data-i18n="unauth.proconnect_btn"></span>
|
||||
</button>
|
||||
<p>
|
||||
<a
|
||||
href="https://www.proconnect.gouv.fr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title="Qu’est-ce que ProConnect ? - nouvelle fenêtre"
|
||||
>
|
||||
Qu’est-ce que ProConnect ?
|
||||
</a>
|
||||
<a href="https://www.proconnect.gouv.fr/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-i18n-attr="title:unauth.proconnect_link_title"
|
||||
data-i18n="unauth.proconnect_link"
|
||||
></a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Authenticated -->
|
||||
<div id="view-auth" style="display:none;">
|
||||
<div id="btn-container">
|
||||
<button id="btn-generate">Ajouter une réunion <span data-app-name></span></button>
|
||||
<button id="btn-disconnect">Se déconnecter</button>
|
||||
<!-- shown when no meeting is present -->
|
||||
<button id="btn-generate" data-i18n="meeting.add_meeting"></button>
|
||||
<!-- shown when a meeting is already present -->
|
||||
<button id="btn-remove" style="display:none;" data-i18n="meeting.remove_meeting"></button>
|
||||
<button id="btn-disconnect" data-i18n="auth.disconnect"></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer id="version-tag">
|
||||
<span class="version-badge">alpha</span>
|
||||
<span class="version-number">0.0.1</span>
|
||||
<a id="feedback-link"
|
||||
style="display:none;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-i18n="footer.feedback"
|
||||
></a>
|
||||
<div id="footer-right">
|
||||
<span class="version-badge">beta</span>
|
||||
<span class="version-number">0.0.2</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,22 +1,77 @@
|
||||
const { APP_NAME } = require("../common");
|
||||
|
||||
/* global Office */
|
||||
const { APP_NAME, FEEDBACK_FORM } = require("../common");
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { initSession, createRoom } = require("../common/api");
|
||||
const { startPolling } = require("../common/polling");
|
||||
const { openTransitDialog } = require("../common/transitDialog");
|
||||
const { loadSession, saveSession, clearSession } = require("../common/session");
|
||||
|
||||
const { buildMeetingMessage } = require("../common/messageBuilder");
|
||||
const { initI18n, t, translateUI } = require("../common/i18n");
|
||||
const { isMeetingAlreadyAdded, removeMeetingLink } = require("../common/meetingDetector");
|
||||
|
||||
// ── Views ────────────────────────────────────────────────────
|
||||
|
||||
// todo - support loading view while polling
|
||||
// todo - support error view
|
||||
function showView(name) {
|
||||
document.getElementById("view-loading").style.display = "none";
|
||||
document.getElementById("view-unauth").style.display = "none";
|
||||
document.getElementById("view-auth").style.display = "none";
|
||||
document.getElementById(`view-${name}`).style.display = "block";
|
||||
|
||||
if (name === "auth") {
|
||||
_refreshMeetingButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Button state ─────────────────────────────────────────────
|
||||
|
||||
function _showAddButton() {
|
||||
document.getElementById("btn-generate").style.display = "block";
|
||||
document.getElementById("btn-remove").style.display = "none";
|
||||
}
|
||||
|
||||
function _showRemoveButton() {
|
||||
document.getElementById("btn-generate").style.display = "none";
|
||||
document.getElementById("btn-remove").style.display = "block";
|
||||
}
|
||||
|
||||
function _setButtonLoading() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = true;
|
||||
btn.textContent = t("meeting.generating");
|
||||
}
|
||||
|
||||
function _setButtonIdle() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = false;
|
||||
btn.textContent = t("meeting.add_meeting", { app_name: APP_NAME });
|
||||
}
|
||||
|
||||
function _setRemoveLoading() {
|
||||
const btn = document.getElementById("btn-remove");
|
||||
btn.disabled = true;
|
||||
btn.textContent = t("meeting.removing");
|
||||
}
|
||||
|
||||
function _setRemoveIdle() {
|
||||
const btn = document.getElementById("btn-remove");
|
||||
btn.disabled = false;
|
||||
btn.textContent = t("meeting.remove_meeting", { app_name: APP_NAME });
|
||||
}
|
||||
|
||||
function _refreshMeetingButtonState() {
|
||||
const item = Office.context.mailbox.item;
|
||||
if (!item) return;
|
||||
isMeetingAlreadyAdded(item).then((alreadyAdded) => {
|
||||
if (alreadyAdded) {
|
||||
_showRemoveButton();
|
||||
} else {
|
||||
_showAddButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Auth ─────────────────────────────────────────────────────
|
||||
|
||||
function connect() {
|
||||
initSession()
|
||||
.then((data) => {
|
||||
@@ -47,22 +102,11 @@ function disconnect() {
|
||||
clearSession().finally(() => showView("unauth"));
|
||||
}
|
||||
|
||||
function _setButtonLoading() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "Génération...";
|
||||
}
|
||||
|
||||
function _setButtonIdle() {
|
||||
const btn = document.getElementById("btn-generate");
|
||||
btn.disabled = false;
|
||||
btn.textContent = `Ajouter une réunion ${APP_NAME}`;
|
||||
}
|
||||
// ── Meeting ──────────────────────────────────────────────────
|
||||
|
||||
function generateMeetingLink() {
|
||||
const session = loadSession();
|
||||
if (!session?.access_token) {
|
||||
console.error("Session introuvable. Veuillez vous reconnecter.");
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
@@ -71,36 +115,28 @@ function generateMeetingLink() {
|
||||
|
||||
createRoom(session)
|
||||
.then((data) => {
|
||||
const { url, message } = buildMeetingMessage(data);
|
||||
const isWeb = Office.context.diagnostics.platform === "OfficeOnline";
|
||||
const { url, text } = buildMeetingMessage(data, isWeb);
|
||||
const item = Office.context.mailbox.item;
|
||||
const coercionType = isWeb ? Office.CoercionType.Html : Office.CoercionType.Text;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
item.body.getAsync(Office.CoercionType.Html, (getResult) => {
|
||||
if (getResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(getResult.error);
|
||||
item.body.setSelectedDataAsync(text, { coercionType }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(setResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
item.body.setAsync(
|
||||
getResult.value + message,
|
||||
{ coercionType: Office.CoercionType.Html },
|
||||
(setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(setResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// ─── If calendar event, also set location ──────────────
|
||||
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
|
||||
item.location.setAsync(url, () => resolve());
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
|
||||
item.location.setAsync(url, () => resolve());
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
_showRemoveButton();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
@@ -109,7 +145,41 @@ function generateMeetingLink() {
|
||||
});
|
||||
}
|
||||
|
||||
Office.onReady((info) => {
|
||||
function removeMeetingLinkFromItem() {
|
||||
const session = loadSession();
|
||||
if (!session?.access_token) {
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
|
||||
_setRemoveLoading();
|
||||
|
||||
const item = Office.context.mailbox.item;
|
||||
|
||||
removeMeetingLink(item)
|
||||
.then(() => {
|
||||
_showAddButton();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
_setRemoveIdle();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Init ─────────────────────────────────────────────────────
|
||||
|
||||
Office.onReady(async (info) => {
|
||||
await initI18n();
|
||||
translateUI();
|
||||
|
||||
if (FEEDBACK_FORM) {
|
||||
const link = document.getElementById("feedback-link");
|
||||
link.href = FEEDBACK_FORM;
|
||||
link.style.display = "inline";
|
||||
}
|
||||
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
document.getElementById("sideload-msg").style.display = "none";
|
||||
@@ -117,10 +187,11 @@ Office.onReady((info) => {
|
||||
document.getElementById("btn-connect").onclick = connect;
|
||||
document.getElementById("btn-disconnect").onclick = disconnect;
|
||||
document.getElementById("btn-generate").onclick = generateMeetingLink;
|
||||
document.getElementById("btn-remove").onclick = removeMeetingLinkFromItem;
|
||||
|
||||
const session = loadSession();
|
||||
if (session?.state === "authenticated" && session?.access_token) {
|
||||
showView("auth");
|
||||
showView("auth"); // this already calls _refreshMeetingButtonState internally
|
||||
} else {
|
||||
showView("unauth");
|
||||
}
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg">Veuillez charger le complément.</div>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div
|
||||
class="spinner-container"
|
||||
role="progressbar"
|
||||
aria-label="Chargement..."
|
||||
data-i18n-aria="app.loading"
|
||||
>
|
||||
<svg
|
||||
class="spinner-svg"
|
||||
|
||||
@@ -2,6 +2,7 @@ const { applyAppName } = require("../common/helpers");
|
||||
const { URLS } = require("../common/urls");
|
||||
const { save } = require("../common/transitToken");
|
||||
const { DIALOG_SIGNALS } = require("../common/transitDialog");
|
||||
const { initI18n, translateUI } = require("../common/i18n");
|
||||
|
||||
// Initiate the authentication flow, then return to the success page
|
||||
function getAuthenticateUrl() {
|
||||
@@ -10,7 +11,11 @@ function getAuthenticateUrl() {
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
Office.onReady(function (info) {
|
||||
Office.onReady(async function (info) {
|
||||
|
||||
await initI18n();
|
||||
translateUI();
|
||||
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.19.0"
|
||||
version = "1.21.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.5.13",
|
||||
|
||||
Generated
+1
-1
@@ -9,7 +9,7 @@ resolution-markers = [
|
||||
|
||||
[[package]]
|
||||
name = "agents"
|
||||
version = "1.19.0"
|
||||
version = "1.21.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "livekit-agents" },
|
||||
|
||||
@@ -46,12 +46,15 @@ from core.recording.event.exceptions import (
|
||||
InvalidFileTypeError,
|
||||
ParsingEventDataError,
|
||||
)
|
||||
from core.recording.event.notification import notification_service
|
||||
from core.recording.event.parsers import get_parser
|
||||
from core.recording.services.metadata_collector import (
|
||||
MetadataCollectorException,
|
||||
MetadataCollectorService,
|
||||
)
|
||||
from core.recording.services.recording_events import (
|
||||
RecordingEventsService,
|
||||
RecordingNotSavableError,
|
||||
)
|
||||
from core.recording.worker.exceptions import (
|
||||
RecordingStartError,
|
||||
RecordingStopError,
|
||||
@@ -972,24 +975,15 @@ class RecordingViewSet(
|
||||
except models.Recording.DoesNotExist as e:
|
||||
raise drf_exceptions.NotFound("No recording found for this event.") from e
|
||||
|
||||
if not recording.is_savable():
|
||||
# Save recording
|
||||
recording_events_service = RecordingEventsService()
|
||||
try:
|
||||
recording_events_service.handle_complete(recording)
|
||||
except RecordingNotSavableError:
|
||||
raise drf_exceptions.PermissionDenied(
|
||||
f"Recording with ID {recording_id} cannot be saved because it is either,"
|
||||
" in an error state or has already been saved."
|
||||
)
|
||||
|
||||
# Attempt to notify external services about the recording
|
||||
# This is a non-blocking operation - failures are logged but don't interrupt the flow
|
||||
notification_succeeded = notification_service.notify_external_services(
|
||||
recording
|
||||
)
|
||||
|
||||
recording.status = (
|
||||
models.RecordingStatusChoices.NOTIFICATION_SUCCEEDED
|
||||
if notification_succeeded
|
||||
else models.RecordingStatusChoices.SAVED
|
||||
)
|
||||
recording.save()
|
||||
) from None
|
||||
|
||||
return drf_response.Response(
|
||||
{"message": "Event processed."},
|
||||
@@ -1187,7 +1181,10 @@ class FileViewSet(
|
||||
serializer.save(creator=self.request.user)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
"""Override to implement a soft delete instead of dumping the record in database."""
|
||||
"""Override to implement a soft delete instead of dumping the record in database.
|
||||
|
||||
Files are actually purged by commands that should run periodically.
|
||||
"""
|
||||
instance.soft_delete()
|
||||
|
||||
@decorators.action(detail=True, methods=["post"], url_path="upload-ended")
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Clean stale pending files that were never fully uploaded."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import File, FileUploadStateChoices
|
||||
from core.tasks.file import process_file_deletion
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""Remove pending files older than a given threshold."""
|
||||
|
||||
help = "Delete pending files that have been stuck for too long"
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"--hours",
|
||||
type=int,
|
||||
default=24,
|
||||
help="Age threshold in hours (default: 24)",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
hours = options["hours"]
|
||||
if hours < 0:
|
||||
raise CommandError("Hours must be greater than 0")
|
||||
|
||||
threshold = timezone.now() - timedelta(hours=hours)
|
||||
|
||||
files = File.objects.filter(
|
||||
upload_state=FileUploadStateChoices.PENDING,
|
||||
created_at__lt=threshold,
|
||||
hard_deleted_at__isnull=True,
|
||||
)
|
||||
|
||||
count = 0
|
||||
for file in files.iterator():
|
||||
# This check shouldn't happen, but just in case we do it to avoid an error
|
||||
if not file.deleted_at:
|
||||
file.soft_delete()
|
||||
file.hard_delete()
|
||||
process_file_deletion(file.id)
|
||||
count += 1
|
||||
|
||||
self.stdout.write(f"Cleaned {count} stale pending file(s).")
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Purge deleted files."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
from core.models import File
|
||||
from core.tasks.file import process_file_deletion
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Purge deleted files (object storage and database object):
|
||||
- files marked as hard deleted in database
|
||||
- files marked as soft deleted and for which the trashbin retention period has expired
|
||||
"""
|
||||
|
||||
help = "Purge deleted files"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Browse purgeable files and queue them through the file deletion task."""
|
||||
|
||||
is_hard_deleted = Q(hard_deleted_at__isnull=False)
|
||||
is_purgeable = Q(
|
||||
deleted_at__lte=timezone.now()
|
||||
- timedelta(days=settings.FILE_PURGE_GRACE_DAYS)
|
||||
)
|
||||
|
||||
count = 0
|
||||
for file in File.objects.filter(is_hard_deleted | is_purgeable).iterator():
|
||||
if file.hard_deleted_at is None:
|
||||
file.hard_delete()
|
||||
|
||||
process_file_deletion.delay(file.id)
|
||||
count += 1
|
||||
|
||||
self.stdout.write(f"Purged {count} deleted file(s).")
|
||||
@@ -58,7 +58,7 @@ class EventParser(Protocol):
|
||||
def parse(self, data: Dict) -> StorageEvent:
|
||||
"""Extract storage event data from raw dictionary input."""
|
||||
|
||||
def validate(self, data: StorageEvent) -> None:
|
||||
def validate(self, data: StorageEvent) -> str:
|
||||
"""Verify storage event data meets all requirements."""
|
||||
|
||||
def get_recording_id(self, data: Dict) -> str:
|
||||
|
||||
@@ -8,6 +8,7 @@ from livekit import api
|
||||
|
||||
from core import models, utils
|
||||
from core.models import Recording
|
||||
from core.recording.event.notification import notification_service
|
||||
|
||||
logger = getLogger(__name__)
|
||||
|
||||
@@ -16,6 +17,10 @@ class RecordingEventsError(Exception):
|
||||
"""Recording event handling fails."""
|
||||
|
||||
|
||||
class RecordingNotSavableError(Exception):
|
||||
"""Recording cannot be saved because it is either in an error state or has already been saved"""
|
||||
|
||||
|
||||
class RecordingEventsService:
|
||||
"""Handles recording-related LiveKit webhook events."""
|
||||
|
||||
@@ -73,3 +78,23 @@ class RecordingEventsService:
|
||||
f"Failed to notify participants in room '{recording.room.id}' about "
|
||||
f"recording limit reached (recording_id={recording.id})"
|
||||
) from e
|
||||
|
||||
@staticmethod
|
||||
def handle_complete(recording: Recording):
|
||||
"""Notify external services and save recording."""
|
||||
|
||||
if not recording.is_savable():
|
||||
raise RecordingNotSavableError
|
||||
|
||||
# Attempt to notify external services about the recording
|
||||
# This is a non-blocking operation - failures are logged but don't interrupt the flow
|
||||
notification_succeeded = notification_service.notify_external_services(
|
||||
recording
|
||||
)
|
||||
|
||||
recording.status = (
|
||||
models.RecordingStatusChoices.NOTIFICATION_SUCCEEDED
|
||||
if notification_succeeded
|
||||
else models.RecordingStatusChoices.SAVED
|
||||
)
|
||||
recording.save()
|
||||
|
||||
@@ -19,6 +19,7 @@ from core.recording.services.metadata_collector import (
|
||||
from core.recording.services.recording_events import (
|
||||
RecordingEventsError,
|
||||
RecordingEventsService,
|
||||
RecordingNotSavableError,
|
||||
)
|
||||
|
||||
from .lobby import LobbyService
|
||||
@@ -88,6 +89,13 @@ class LiveKitEventsService:
|
||||
def __init__(self):
|
||||
"""Initialize with required services."""
|
||||
|
||||
self._webhook_handlers = {
|
||||
"egress_updated": self._handle_egress_updated,
|
||||
"egress_ended": self._handle_egress_ended,
|
||||
"room_started": self._handle_room_started,
|
||||
"room_finished": self._handle_room_finished,
|
||||
}
|
||||
|
||||
token_verifier = api.TokenVerifier(
|
||||
settings.LIVEKIT_CONFIGURATION["api_key"],
|
||||
settings.LIVEKIT_CONFIGURATION["api_secret"],
|
||||
@@ -135,14 +143,11 @@ class LiveKitEventsService:
|
||||
f"Unknown webhook type: {data.event}"
|
||||
) from e
|
||||
|
||||
handler_name = f"_handle_{webhook_type.value}"
|
||||
handler = getattr(self, handler_name, None)
|
||||
# Handle according to received webhook type
|
||||
handler = self._webhook_handlers.get(webhook_type.value)
|
||||
|
||||
if not handler or not callable(handler):
|
||||
return
|
||||
|
||||
# pylint: disable=not-callable
|
||||
handler(data)
|
||||
if handler is not None:
|
||||
handler(data)
|
||||
|
||||
def _handle_egress_updated(self, data):
|
||||
"""Handle 'egress_updated' event."""
|
||||
@@ -195,6 +200,24 @@ class LiveKitEventsService:
|
||||
f"Failed to process limit reached event for recording {recording}"
|
||||
) from e
|
||||
|
||||
# Fallback for completion when no MinIO/S3 webhooks are configured
|
||||
if (
|
||||
not settings.RECORDING_STORAGE_EVENT_ENABLE
|
||||
) and data.egress_info.status in [
|
||||
api.EgressStatus.EGRESS_COMPLETE,
|
||||
api.EgressStatus.EGRESS_LIMIT_REACHED,
|
||||
]:
|
||||
try:
|
||||
self.recording_events.handle_complete(recording)
|
||||
except RecordingNotSavableError:
|
||||
logger.warning(
|
||||
"Recording %s is not savable on egress complete "
|
||||
"(already saved or in an error state); ignoring.",
|
||||
recording.id,
|
||||
)
|
||||
|
||||
# Silently ignoring EGRESS_ABORTED, EGRESS_FAILED
|
||||
|
||||
def _handle_room_started(self, data):
|
||||
"""Handle 'room_started' event."""
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Tests for the clean_pending_files management command."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_clean_pending_files_no_stale_files():
|
||||
"""Nothing happens when there are no stale pending files."""
|
||||
call_command("clean_pending_files")
|
||||
|
||||
|
||||
def test_clean_pending_files_recent_pending_not_deleted():
|
||||
"""Recent pending files (within threshold) should not be deleted."""
|
||||
file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
update_upload_state=models.FileUploadStateChoices.PENDING,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
|
||||
call_command("clean_pending_files")
|
||||
|
||||
file.refresh_from_db()
|
||||
assert file.deleted_at is None
|
||||
assert default_storage.exists(file.file_key)
|
||||
|
||||
|
||||
def test_clean_pending_files_old_pending_deleted():
|
||||
"""Pending files older than the threshold should be deleted."""
|
||||
old_date = timezone.now() - timedelta(hours=49)
|
||||
file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
update_upload_state=models.FileUploadStateChoices.PENDING,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
assert default_storage.exists(file.file_key)
|
||||
models.File.objects.filter(pk=file.pk).update(created_at=old_date)
|
||||
|
||||
call_command("clean_pending_files")
|
||||
|
||||
assert not models.File.objects.filter(pk=file.pk).exists()
|
||||
assert not default_storage.exists(file.file_key)
|
||||
|
||||
|
||||
def test_clean_pending_files_old_non_pending_not_deleted():
|
||||
"""Old files that are not pending should not be deleted."""
|
||||
old_date = timezone.now() - timedelta(hours=49)
|
||||
file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
update_upload_state=models.FileUploadStateChoices.READY,
|
||||
)
|
||||
models.File.objects.filter(pk=file.pk).update(created_at=old_date)
|
||||
|
||||
call_command("clean_pending_files")
|
||||
|
||||
file.refresh_from_db()
|
||||
assert file.deleted_at is None
|
||||
assert file.hard_deleted_at is None
|
||||
|
||||
|
||||
def test_clean_pending_files_custom_hours():
|
||||
"""The --hours argument controls the age threshold."""
|
||||
old_date = timezone.now() - timedelta(hours=10)
|
||||
file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
update_upload_state=models.FileUploadStateChoices.PENDING,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
models.File.objects.filter(pk=file.pk).update(created_at=old_date)
|
||||
|
||||
# Default 24h threshold -> file not deleted
|
||||
call_command("clean_pending_files")
|
||||
|
||||
file.refresh_from_db()
|
||||
assert file.deleted_at is None
|
||||
assert default_storage.exists(file.file_key)
|
||||
|
||||
# 8h threshold -> file deleted
|
||||
call_command("clean_pending_files", "--hours=8")
|
||||
|
||||
assert not models.File.objects.filter(pk=file.pk).exists()
|
||||
assert not default_storage.exists(file.file_key)
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Tests for the purge_deleted_files management command."""
|
||||
|
||||
from datetime import timedelta
|
||||
from io import StringIO
|
||||
from random import randint
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.core.files.storage import default_storage
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from core import factories, models
|
||||
from core.tasks.file import process_file_deletion
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def test_purge_deleted_files_no_deleted_files(django_assert_num_queries):
|
||||
"""Nothing happens when there are no purgeable files."""
|
||||
with django_assert_num_queries(1):
|
||||
call_command("purge_deleted_files")
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
def test_purge_deleted_files_success(settings):
|
||||
"""
|
||||
Queue deletion for:
|
||||
- hard-deleted files
|
||||
- soft-deleted files past retention period + grace period.
|
||||
"""
|
||||
out = StringIO()
|
||||
|
||||
settings.FILE_PURGE_GRACE_DAYS = grace = randint(1, 20)
|
||||
|
||||
now = timezone.now()
|
||||
purge_now = now - timedelta(days=grace)
|
||||
|
||||
not_deleted_file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=now):
|
||||
not_purgeable_file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
not_purgeable_file.soft_delete()
|
||||
|
||||
with patch("django.utils.timezone.now", return_value=purge_now):
|
||||
purgeable_file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
purgeable_file.soft_delete()
|
||||
|
||||
hard_deleted_file = factories.FileFactory(
|
||||
type=models.FileTypeChoices.BACKGROUND_IMAGE,
|
||||
upload_bytes=b"hello",
|
||||
)
|
||||
hard_deleted_file.soft_delete()
|
||||
hard_deleted_file.hard_delete()
|
||||
|
||||
with patch(
|
||||
"core.management.commands.purge_deleted_files.process_file_deletion.delay",
|
||||
side_effect=process_file_deletion,
|
||||
) as mock_delay:
|
||||
call_command("purge_deleted_files", stdout=out)
|
||||
|
||||
assert "Purged 2 deleted file(s)." in out.getvalue()
|
||||
assert mock_delay.call_count == 2
|
||||
called_ids = {call.args[0] for call in mock_delay.call_args_list}
|
||||
assert called_ids == {purgeable_file.id, hard_deleted_file.id}
|
||||
|
||||
assert models.File.objects.filter(id=not_deleted_file.id).exists()
|
||||
assert models.File.objects.filter(id=not_purgeable_file.id).exists()
|
||||
assert not models.File.objects.filter(id=purgeable_file.id).exists()
|
||||
assert not models.File.objects.filter(id=hard_deleted_file.id).exists()
|
||||
|
||||
assert default_storage.exists(not_deleted_file.file_key)
|
||||
assert default_storage.exists(not_purgeable_file.file_key)
|
||||
assert not default_storage.exists(purgeable_file.file_key)
|
||||
assert not default_storage.exists(hard_deleted_file.file_key)
|
||||
@@ -224,3 +224,44 @@ def test_save_recording_success(recording_settings, mock_get_parser, client, sta
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == RecordingStatusChoices.SAVED
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.recording.services.recording_events.notification_service."
|
||||
"notify_external_services"
|
||||
)
|
||||
@pytest.mark.parametrize("notification_succeeded", [True, False])
|
||||
def test_save_recording_notifies_external_services(
|
||||
mock_notify_external_services,
|
||||
recording_settings,
|
||||
mock_get_parser,
|
||||
client,
|
||||
notification_succeeded,
|
||||
):
|
||||
"""External services should be notified when a recording is saved."""
|
||||
|
||||
recording = RecordingFactory(status="active")
|
||||
|
||||
mock_parser = mock.Mock()
|
||||
mock_parser.get_recording_id.return_value = recording.id
|
||||
mock_get_parser.return_value = mock_parser
|
||||
|
||||
mock_notify_external_services.return_value = notification_succeeded
|
||||
|
||||
response = client.post(
|
||||
"/api/v1.0/recordings/storage-hook/",
|
||||
{"recording_data": "valid-data"},
|
||||
HTTP_AUTHORIZATION="Bearer testAuthToken",
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"message": "Event processed."}
|
||||
|
||||
mock_notify_external_services.assert_called_once_with(recording)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == (
|
||||
RecordingStatusChoices.NOTIFICATION_SUCCEEDED
|
||||
if notification_succeeded
|
||||
else RecordingStatusChoices.SAVED
|
||||
)
|
||||
|
||||
@@ -91,7 +91,9 @@ def test_handle_egress_ended_success(
|
||||
)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "stopped"
|
||||
|
||||
# NB: notify_external_services will return False, so status is "saved"
|
||||
assert recording.status == "saved"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -155,7 +157,7 @@ def test_handle_egress_updated_non_handled(
|
||||
def test_handle_egress_ended_metadata_update_fails(
|
||||
mock_update_room_metadata, mock_notify, mode, notification_type, service
|
||||
):
|
||||
"""Should successfully stop recording when metadata's update fails."""
|
||||
"""Should successfully stop and save recording when metadata's update fails."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", mode=mode, status="active")
|
||||
mock_data = mock.MagicMock()
|
||||
@@ -170,7 +172,9 @@ def test_handle_egress_ended_metadata_update_fails(
|
||||
room_name=str(recording.room.id), notification_data={"type": notification_type}
|
||||
)
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "stopped"
|
||||
|
||||
# NB: notify_external_services will return False, so status is "saved"
|
||||
assert recording.status == "saved"
|
||||
|
||||
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@@ -326,6 +330,143 @@ def test_handle_egress_ended_does_not_call_metadata_collector_stop_when_conditio
|
||||
mock_collector.stop.assert_not_called()
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.recording.services.recording_events.notification_service."
|
||||
"notify_external_services"
|
||||
)
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
@pytest.mark.parametrize(
|
||||
"egress_status",
|
||||
[EgressStatus.EGRESS_COMPLETE, EgressStatus.EGRESS_LIMIT_REACHED],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"notify_return_value, recording_status",
|
||||
[(True, "notification_succeeded"), (False, "saved")],
|
||||
)
|
||||
def test_handle_egress_ended_finalizes_recording( # noqa: PLR0913
|
||||
mock_update_room_metadata,
|
||||
mock_notify,
|
||||
mock_notify_external_services,
|
||||
notify_return_value,
|
||||
recording_status,
|
||||
egress_status,
|
||||
service,
|
||||
settings,
|
||||
): # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
"""Should notify external services and save the recording on egress completion
|
||||
(EGRESS_COMPLETE or EGRESS_LIMIT_REACHED) when RECORDING_STORAGE_EVENT_ENABLE is False.
|
||||
"""
|
||||
settings.RECORDING_STORAGE_EVENT_ENABLE = False
|
||||
mock_notify_external_services.return_value = notify_return_value
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="active")
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = egress_status
|
||||
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
mock_notify_external_services.assert_called_once_with(recording)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == recording_status
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"core.recording.services.recording_events.notification_service."
|
||||
"notify_external_services"
|
||||
)
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
@pytest.mark.parametrize(
|
||||
"egress_status, expected_status",
|
||||
[
|
||||
(EgressStatus.EGRESS_COMPLETE, "active"),
|
||||
(EgressStatus.EGRESS_LIMIT_REACHED, "stopped"),
|
||||
],
|
||||
)
|
||||
def test_handle_egress_ended_does_not_finalize_when_webhooks_enabled( # noqa: PLR0913
|
||||
mock_update_room_metadata,
|
||||
mock_notify,
|
||||
mock_notify_external_services,
|
||||
egress_status,
|
||||
expected_status,
|
||||
service,
|
||||
settings,
|
||||
): # pylint: disable=too-many-arguments,too-many-positional-arguments
|
||||
"""When storage event webhooks are enabled, egress_ended must not finalize the
|
||||
recording: external services are never notified. EGRESS_LIMIT_REACHED still stops
|
||||
the recording, EGRESS_COMPLETE leaves it active.
|
||||
"""
|
||||
settings.RECORDING_STORAGE_EVENT_ENABLE = True
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="active")
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = egress_status
|
||||
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
mock_notify_external_services.assert_not_called()
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == expected_status
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"egress_status",
|
||||
[
|
||||
EgressStatus.EGRESS_STARTING,
|
||||
EgressStatus.EGRESS_ACTIVE,
|
||||
EgressStatus.EGRESS_ENDING,
|
||||
EgressStatus.EGRESS_FAILED,
|
||||
EgressStatus.EGRESS_ABORTED,
|
||||
],
|
||||
)
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_does_not_save_on_wrong_status(
|
||||
mock_update_room_metadata, egress_status, service, settings
|
||||
):
|
||||
"""Shouldn't save on invalid status."""
|
||||
settings.RECORDING_STORAGE_EVENT_ENABLE = False
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="active")
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = egress_status
|
||||
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "active"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status", ["failed_to_start", "aborted", "failed_to_stop", "saved", "initiated"]
|
||||
)
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_ignores_non_savable_recording(
|
||||
mock_update_room_metadata, status, service, settings
|
||||
):
|
||||
"""Should handle non-savable recordings idempotently without raising.
|
||||
|
||||
'egress_ended' may be redelivered (e.g. for an already-saved recording);
|
||||
this must not raise, otherwise the webhook would 500 and LiveKit would retry.
|
||||
"""
|
||||
settings.RECORDING_STORAGE_EVENT_ENABLE = False
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status=status)
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = EgressStatus.EGRESS_COMPLETE
|
||||
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == status
|
||||
|
||||
|
||||
@mock.patch.object(LobbyService, "clear_room_cache")
|
||||
@mock.patch.object(TelephonyService, "delete_dispatch_rule")
|
||||
def test_handle_room_finished_clears_cache_and_deletes_dispatch_rule(
|
||||
|
||||
@@ -182,7 +182,9 @@ class Base(Configuration):
|
||||
environ_name="FILE_UPLOAD_ENABLED",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
FILE_PURGE_GRACE_DAYS = values.PositiveIntegerValue(
|
||||
default=7, environ_name="FILE_PURGE_GRACE_DAYS", environ_prefix=None
|
||||
)
|
||||
FILE_UPLOAD_PATH = values.Value(
|
||||
"files", environ_name="FILE_UPLOAD_PATH", environ_prefix=None
|
||||
)
|
||||
@@ -411,6 +413,12 @@ class Base(Configuration):
|
||||
"transcription_destination": values.Value(
|
||||
None, environ_name="FRONTEND_TRANSCRIPTION_DESTINATION", environ_prefix=None
|
||||
),
|
||||
"max_participants_for_sound": values.PositiveIntegerValue(
|
||||
5, environ_name="FRONTEND_MAX_PARTICIPANTS_FOR_SOUND", environ_prefix=None
|
||||
),
|
||||
"auto_mute_on_join_threshold": values.PositiveIntegerValue(
|
||||
50, environ_name="FRONTEND_AUTO_MUTE_ON_JOIN_THRESHOLD", environ_prefix=None
|
||||
),
|
||||
}
|
||||
|
||||
# Mail
|
||||
|
||||
@@ -7,7 +7,7 @@ build-backend = "uv_build"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.19.0"
|
||||
version = "1.21.0"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -40,7 +40,7 @@ dependencies = [
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django-pydantic-field==0.5.4",
|
||||
"django==5.2.14",
|
||||
"django==5.2.15",
|
||||
"djangorestframework==3.17.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"dockerflow==2026.3.4",
|
||||
|
||||
Generated
+46
-49
@@ -503,55 +503,52 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.7"
|
||||
version = "49.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -586,16 +583,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.2.14"
|
||||
version = "5.2.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "asgiref" },
|
||||
{ name = "sqlparse" },
|
||||
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/95/95f7faa0950867afaa0bef2460c6263afd6a2c78cc9434046ed28160b015/django-5.2.14.tar.gz", hash = "sha256:58a63ba841662e5c686b57ba1fec52ddd68c0b93bd96ac3029d55728f00bf8a2", size = 10895118, upload-time = "2026-05-05T13:57:31.104Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2b/e3/31722f7284c9f43333daff9aee9184678e4487adcb5506af0db8cea09ce1/django-5.2.15.tar.gz", hash = "sha256:5154a9bf84ac01dde011e367f355c07dbb329532e06810dcf3ef2af269e236e7", size = 10873669, upload-time = "2026-06-03T13:03:35.892Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/14/44/f172870cf87aa25afef48fb72adba89ee8b77fcab6f3b23d240b923f1528/django-5.2.14-py3-none-any.whl", hash = "sha256:6f712143bd3064310d1f50fac859c3e9a274bdcfc9595339853be7779297fc76", size = 8311320, upload-time = "2026-05-05T13:57:25.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/38140b1643c00d5c46ce69c78e6980fd285aee223100319631bedee4f5e7/django-5.2.15-py3-none-any.whl", hash = "sha256:0eb4a9bb1853a35b0286dbc6d916bd352c8c2687195a7f2d6f80cefd840e4970", size = 8311957, upload-time = "2026-06-03T13:03:31.329Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1187,7 +1184,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "meet"
|
||||
version = "1.19.0"
|
||||
version = "1.21.0"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
@@ -1259,7 +1256,7 @@ requires-dist = [
|
||||
{ name = "brotli", specifier = "==1.2.0" },
|
||||
{ name = "celery", extras = ["redis"], specifier = "==5.6.3" },
|
||||
{ name = "dj-database-url", specifier = "==3.1.2" },
|
||||
{ name = "django", specifier = "==5.2.14" },
|
||||
{ name = "django", specifier = "==5.2.15" },
|
||||
{ name = "django-configurations", specifier = "==2.5.1" },
|
||||
{ name = "django-cors-headers", specifier = "==4.9.0" },
|
||||
{ name = "django-countries", specifier = "==8.2.0" },
|
||||
|
||||
@@ -45,6 +45,8 @@ USER root
|
||||
|
||||
# Security patches for known CVEs
|
||||
RUN apk update && apk upgrade \
|
||||
libcrypto3>=3.5.7-r0 \
|
||||
libssl3>=3.5.7-r0 \
|
||||
musl \
|
||||
musl-utils \
|
||||
zlib>=1.3.2-r0 \
|
||||
|
||||
Generated
+83
-377
@@ -1,20 +1,21 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "meet",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@libreaudio/la-call": "0.1.4",
|
||||
"@livekit/components-react": "2.9.21",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
"@livekit/track-processors": "0.7.2",
|
||||
"@pandacss/preset-panda": "1.11.1",
|
||||
"@pandacss/preset-panda": "1.11.3",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.10.0",
|
||||
"@remixicon/react": "4.9.0",
|
||||
@@ -29,11 +30,11 @@
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.19.0",
|
||||
"posthog-js": "1.376.0",
|
||||
"posthog-js": "1.382.0",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.14.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"react-i18next": "17.0.8",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
"wouter": "3.10.0"
|
||||
@@ -338,9 +339,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
|
||||
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
"integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1446,6 +1447,12 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@libreaudio/la-call": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@libreaudio/la-call/-/la-call-0.1.4.tgz",
|
||||
"integrity": "sha512-yaqlYGpvZbfpr1h2p1jSGOKPUy7NsfpoJN9gXvHmJu5MB8nN/qfx6LQ2a0hbua3H8rargFKROOv4VZ5WuTKNsA==",
|
||||
"license": "GPL-3.0+"
|
||||
},
|
||||
"node_modules/@livekit/components-core": {
|
||||
"version": "0.12.13",
|
||||
"resolved": "https://registry.npmjs.org/@livekit/components-core/-/components-core-0.12.13.tgz",
|
||||
@@ -1654,252 +1661,6 @@
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
|
||||
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/api-logs": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
|
||||
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/core": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
|
||||
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
|
||||
"integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/otlp-exporter-base": "0.208.0",
|
||||
"@opentelemetry/otlp-transformer": "0.208.0",
|
||||
"@opentelemetry/sdk-logs": "0.208.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-exporter-base": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
|
||||
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/otlp-transformer": "0.208.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-transformer": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
|
||||
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/sdk-logs": "0.208.0",
|
||||
"@opentelemetry/sdk-metrics": "2.2.0",
|
||||
"@opentelemetry/sdk-trace-base": "2.2.0",
|
||||
"protobufjs": "^7.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": "^1.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.1.tgz",
|
||||
"integrity": "sha512-BViBCdE/GuXRlp9k7nS1w6wJvY5fnFX5XvuEtWsTAOQFIO89Eru7lGW3WbfbxtCuZ/GbrJfAziXG0w0dpxL7eQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.5.1",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.1.tgz",
|
||||
"integrity": "sha512-Dwlc+3HAZqpgTYq0MUyZABjFkcrKTePwuiFVLjahGD8cx3enqihmpAmdgNFO1R4m/sIe5afjJrA25Prqy4NXlA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.0.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-logs": {
|
||||
"version": "0.208.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
|
||||
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api-logs": "0.208.0",
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.4.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-metrics": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
|
||||
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.9.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
|
||||
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/resources": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/sdk-trace-base/node_modules/@opentelemetry/resources": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
|
||||
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@opentelemetry/core": "2.2.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.29.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.19.0 || >=20.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opentelemetry/api": ">=1.3.0 <1.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@opentelemetry/semantic-conventions": {
|
||||
"version": "1.39.0",
|
||||
"resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.39.0.tgz",
|
||||
"integrity": "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@oxc-project/types": {
|
||||
"version": "0.132.0",
|
||||
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz",
|
||||
@@ -1928,6 +1689,16 @@
|
||||
"typescript": "6.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/config/node_modules/@pandacss/preset-panda": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/preset-panda/-/preset-panda-1.11.1.tgz",
|
||||
"integrity": "sha512-+0ZV4d5TZIqbXLh4xRIptuOd/VOc7YncOfeHZn9uWzGKi04uob9I/8mQ/hgSSRWCKmaLpRzsBDuijr2ODD59iA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pandacss/types": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/config/node_modules/typescript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz",
|
||||
@@ -2022,6 +1793,16 @@
|
||||
"pandacss": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/dev/node_modules/@pandacss/preset-panda": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/preset-panda/-/preset-panda-1.11.1.tgz",
|
||||
"integrity": "sha512-+0ZV4d5TZIqbXLh4xRIptuOd/VOc7YncOfeHZn9uWzGKi04uob9I/8mQ/hgSSRWCKmaLpRzsBDuijr2ODD59iA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pandacss/types": "1.11.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/extractor": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/extractor/-/extractor-1.11.1.tgz",
|
||||
@@ -2349,12 +2130,24 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/preset-panda": {
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/preset-panda/-/preset-panda-1.11.1.tgz",
|
||||
"integrity": "sha512-+0ZV4d5TZIqbXLh4xRIptuOd/VOc7YncOfeHZn9uWzGKi04uob9I/8mQ/hgSSRWCKmaLpRzsBDuijr2ODD59iA==",
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/preset-panda/-/preset-panda-1.11.3.tgz",
|
||||
"integrity": "sha512-IHeRQyIYeGKpQl78A/oI9fGg8MHQiOX/HttH+iQNaRU/MbGN6JsPe/N0ekQC6QY8iiKtLhqR+SRc455SN6JVWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@pandacss/types": "1.11.1"
|
||||
"@pandacss/types": "1.11.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/preset-panda/node_modules/@pandacss/types": {
|
||||
"version": "1.11.3",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/types/-/types-1.11.3.tgz",
|
||||
"integrity": "sha512-tUbxa3WzoCuulP6kcMn7f0uNgLBvu81JAg4sfoUpz6PZE36VHA3k9n6ROEwziTbr7vqgeCj5DYQCUldrVNbEGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@pandacss/reporter": {
|
||||
@@ -2411,87 +2204,24 @@
|
||||
"version": "1.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@pandacss/types/-/types-1.11.1.tgz",
|
||||
"integrity": "sha512-VkigbeCwlDyyMBlQeRwOMLshSRjNB/2HqvfZvaVXT4+5miAYUAKeQDR88UqUL2aqPWkltTxMVEwBnqSQdC8+jw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@posthog/core": {
|
||||
"version": "1.29.9",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.29.9.tgz",
|
||||
"integrity": "sha512-DjvuIyBZ2Z/gBhtZlITlM2D8PlnMsHSQ1D78dbUYoVsgGguvanpJTobZObjLlFkybyvfZFYkpoJkFNI/2Pw4IQ==",
|
||||
"version": "1.30.10",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.30.10.tgz",
|
||||
"integrity": "sha512-R7Z5jDB3ugwfSujMmRd5osPPR6L6BqfcaSNcYOekzRMZ4Jklq74p05xByP09EnUvKXb5czI+RQVCITTWRWuFXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@posthog/types": "1.376.0"
|
||||
"@posthog/types": "1.382.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@posthog/types": {
|
||||
"version": "1.376.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.376.0.tgz",
|
||||
"integrity": "sha512-gbFfxCuZDs/D4QZMwdE+smD1jsuqgGpS6yKGHZZ19foxMy8RYHsU1E47iG1b88n/uN02fAabLibVwuxLtq8juw==",
|
||||
"version": "1.382.0",
|
||||
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.382.0.tgz",
|
||||
"integrity": "sha512-iK4OcSgvtmS9FZ9EUpvwlRZmHCLXaZ3+6dbRjkE7q9LL0zHLewxJH84H6uGvCw8aGzxs5rIliZqPHgimTcQEaw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz",
|
||||
"integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
|
||||
"integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz",
|
||||
"integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1",
|
||||
"@protobufjs/inquire": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/inquire": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz",
|
||||
"integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
|
||||
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@react-aria/autocomplete": {
|
||||
"version": "3.0.0-rc.4",
|
||||
"resolved": "https://registry.npmjs.org/@react-aria/autocomplete/-/autocomplete-3.0.0-rc.4.tgz",
|
||||
@@ -4282,7 +4012,10 @@
|
||||
"version": "24.12.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.4.tgz",
|
||||
"integrity": "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~7.16.0"
|
||||
}
|
||||
@@ -9964,12 +9697,6 @@
|
||||
"url": "https://tidelift.com/funding/github/npm/loglevel"
|
||||
}
|
||||
},
|
||||
"node_modules/long": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/look-it-up": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/look-it-up/-/look-it-up-2.1.0.tgz",
|
||||
@@ -10889,18 +10616,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/posthog-js": {
|
||||
"version": "1.376.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.376.0.tgz",
|
||||
"integrity": "sha512-YGfQ6gSmqmEh287PHjXRDJ9zML3Su1UIt1+xjRy7Yk6yW43Sc7sFK3CpCkLchCGhIA4x6VaqK+LaqB+7+MCo7A==",
|
||||
"version": "1.382.0",
|
||||
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.382.0.tgz",
|
||||
"integrity": "sha512-lXwVlNdPLkhDft48ZgLQ5Jf4RsuQVwz7Hr7luD4DAG+4wDGA+k9eE2no36r3Z1w4uK0EmIf9lmav6+4RsmP7nA==",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/api-logs": "^0.208.0",
|
||||
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
|
||||
"@opentelemetry/resources": "^2.2.0",
|
||||
"@opentelemetry/sdk-logs": "^0.208.0",
|
||||
"@posthog/core": "1.29.9",
|
||||
"@posthog/types": "1.376.0",
|
||||
"@posthog/core": "1.30.10",
|
||||
"@posthog/types": "1.382.0",
|
||||
"core-js": "^3.38.1",
|
||||
"dompurify": "^3.3.2",
|
||||
"fflate": "^0.4.8",
|
||||
@@ -10981,30 +10703,6 @@
|
||||
"react-is": "^16.13.1"
|
||||
}
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.4",
|
||||
"@protobufjs/eventemitter": "^1.1.0",
|
||||
"@protobufjs/fetch": "^1.1.0",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/inquire": "^1.1.0",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.0",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -11235,17 +10933,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.1.1.tgz",
|
||||
"integrity": "sha512-R/Vg9wIli2P3FfeI8o1eNJUJue5LWpFsQePCHdQDmX0Co3zkr6kdT8gAseb/yGeWbNz1Txc4bKDQuZYsC0kQfw==",
|
||||
"version": "17.0.8",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
|
||||
"integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.25.0",
|
||||
"html-parse-stringify": "^3.0.1"
|
||||
"@babel/runtime": "^7.29.2",
|
||||
"html-parse-stringify": "^3.0.1",
|
||||
"use-sync-external-store": "^1.6.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"i18next": ">= 23.2.3",
|
||||
"react": ">= 16.8.0"
|
||||
"i18next": ">= 26.2.0",
|
||||
"react": ">= 16.8.0",
|
||||
"typescript": "^5 || ^6"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
@@ -11253,6 +10953,9 @@
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12797,7 +12500,10 @@
|
||||
"version": "7.16.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||
"license": "MIT"
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "2.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -17,11 +17,12 @@
|
||||
"dependencies": {
|
||||
"@fontsource-variable/atkinson-hyperlegible-next": "5.2.6",
|
||||
"@fontsource-variable/lexend": "5.2.11",
|
||||
"@libreaudio/la-call": "0.1.4",
|
||||
"@fontsource/opendyslexic": "5.2.5",
|
||||
"@livekit/components-react": "2.9.21",
|
||||
"@livekit/components-styles": "1.2.0",
|
||||
"@livekit/track-processors": "0.7.2",
|
||||
"@pandacss/preset-panda": "1.11.1",
|
||||
"@pandacss/preset-panda": "1.11.3",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.10.0",
|
||||
"@remixicon/react": "4.9.0",
|
||||
@@ -36,11 +37,11 @@
|
||||
"i18next-parser": "9.4.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"livekit-client": "2.19.0",
|
||||
"posthog-js": "1.376.0",
|
||||
"posthog-js": "1.382.0",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.14.0",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"react-i18next": "17.0.8",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.2",
|
||||
"wouter": "3.10.0"
|
||||
|
||||
@@ -55,6 +55,8 @@ export interface ApiConfig {
|
||||
default_sources: Source[]
|
||||
}
|
||||
transcription_destination?: string
|
||||
max_participants_for_sound: number
|
||||
auto_mute_on_join_threshold: number
|
||||
}
|
||||
|
||||
const fetchConfig = (): Promise<ApiConfig> => {
|
||||
|
||||
@@ -5,6 +5,16 @@ import { type ApiUser } from './ApiUser'
|
||||
import { useMemo } from 'react'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
const SILENT_LOGIN_PARAM = 'silentLogin'
|
||||
|
||||
const isSilentLoginDisabledByUrl = () => {
|
||||
if (typeof window === 'undefined') return false
|
||||
const value = new URLSearchParams(window.location.search).get(
|
||||
SILENT_LOGIN_PARAM
|
||||
)
|
||||
return value === 'false'
|
||||
}
|
||||
|
||||
/**
|
||||
* returns info about currently logged-in user
|
||||
*
|
||||
@@ -17,16 +27,22 @@ export const useUser = (
|
||||
) => {
|
||||
const { data, isLoading: isConfigLoading } = useConfig()
|
||||
|
||||
const disabledByUrl = useMemo(() => isSilentLoginDisabledByUrl(), [])
|
||||
|
||||
const options = useMemo(() => {
|
||||
if (isConfigLoading) return
|
||||
if (data?.is_silent_login_enabled !== true) {
|
||||
|
||||
const silentDisabled =
|
||||
data?.is_silent_login_enabled !== true || disabledByUrl
|
||||
|
||||
if (silentDisabled) {
|
||||
return {
|
||||
...opts,
|
||||
...opts.fetchUserOptions,
|
||||
attemptSilent: false,
|
||||
}
|
||||
}
|
||||
return opts.fetchUserOptions
|
||||
}, [data, opts, isConfigLoading])
|
||||
}, [data, opts, isConfigLoading, disabledByUrl])
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: [keys.user],
|
||||
|
||||
@@ -44,7 +44,8 @@ export function PaginationControl({
|
||||
if (totalPageCount <= 1) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
<nav
|
||||
aria-label={t('label')}
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
bottom: '1rem',
|
||||
@@ -74,7 +75,7 @@ export function PaginationControl({
|
||||
<RiArrowLeftSLine />
|
||||
</Button>
|
||||
<span
|
||||
aria-live="polite"
|
||||
role="status"
|
||||
className={css({
|
||||
padding: '0.25rem 0.5rem',
|
||||
})}
|
||||
@@ -93,6 +94,6 @@ export function PaginationControl({
|
||||
>
|
||||
<RiArrowRightSLine />
|
||||
</Button>
|
||||
</div>
|
||||
</nav>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ import { useScreenReaderAnnounce } from '@/hooks/useScreenReaderAnnounce'
|
||||
import { Emoji } from '@/features/reactions/types'
|
||||
import { useReactions } from '@/features/reactions/hooks/useReactions'
|
||||
import { NotificationProvider } from './NotificationProvider'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
export const MainNotificationToast = () => {
|
||||
const room = useRoomContext()
|
||||
const { data } = useConfig()
|
||||
const { triggerNotificationSound } = useNotificationSound()
|
||||
const { t } = useTranslation('notifications')
|
||||
const announce = useScreenReaderAnnounce()
|
||||
@@ -135,12 +137,21 @@ export const MainNotificationToast = () => {
|
||||
}
|
||||
}, [room, handleEmoji])
|
||||
|
||||
const triggerNotificationSoundIfRoomIsSmall = useCallback(
|
||||
(type: NotificationType) => {
|
||||
if (!data) return
|
||||
if (room.numParticipants >= data.max_participants_for_sound) return
|
||||
triggerNotificationSound(type)
|
||||
},
|
||||
[room, data, triggerNotificationSound]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const showJoinNotification = (participant: Participant) => {
|
||||
if (isMobileBrowser()) {
|
||||
return
|
||||
}
|
||||
triggerNotificationSound(NotificationType.ParticipantJoined)
|
||||
triggerNotificationSoundIfRoomIsSmall(NotificationType.ParticipantJoined)
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
@@ -155,7 +166,7 @@ export const MainNotificationToast = () => {
|
||||
return () => {
|
||||
room.off(RoomEvent.ParticipantConnected, showJoinNotification)
|
||||
}
|
||||
}, [room, triggerNotificationSound])
|
||||
}, [room, triggerNotificationSoundIfRoomIsSmall])
|
||||
|
||||
useEffect(() => {
|
||||
const removeParticipantNotifications = (participant: Participant) => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export enum NotificationType {
|
||||
AutoMuteLargeRoom = 'autoMuteLargeRoom',
|
||||
ParticipantJoined = 'participantJoined',
|
||||
HandRaised = 'handRaised',
|
||||
ParticipantMuted = 'participantMuted',
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { useToast } from '@react-aria/toast'
|
||||
import { useRef } from 'react'
|
||||
|
||||
import { type ToastProps } from './Toast'
|
||||
import { VStack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Button } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { StyledToastContainer } from './StyledToastContainer'
|
||||
import { useRoomContext } from '@livekit/components-react'
|
||||
|
||||
export function ToastAutoMuteLargeRoom({
|
||||
state,
|
||||
...props
|
||||
}: Readonly<ToastProps>) {
|
||||
const room = useRoomContext()
|
||||
const { t } = useTranslation('notifications', {
|
||||
keyPrefix: 'autoMuteLargeRoom',
|
||||
})
|
||||
const ref = useRef(null)
|
||||
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||
const toast = props.toast
|
||||
|
||||
const handleDismiss = async () => {
|
||||
room.localParticipant
|
||||
.setMicrophoneEnabled(true)
|
||||
.finally(() => state.close(toast.key))
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<VStack
|
||||
justify="start"
|
||||
alignItems="self-start"
|
||||
{...contentProps}
|
||||
maxWidth="370px"
|
||||
gap="0.75rem"
|
||||
padding={14}
|
||||
>
|
||||
<p>{t('auto')}</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="text"
|
||||
className={css({
|
||||
color: 'primary.300',
|
||||
})}
|
||||
onPress={() => handleDismiss()}
|
||||
>
|
||||
{t('dismiss')}
|
||||
</Button>
|
||||
</VStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { ToastAnyRecording } from './ToastAnyRecording'
|
||||
import { ToastRecordingSaving } from './ToastRecordingSaving'
|
||||
import { ToastPermissionsRemoved } from './ToastPermissionsRemoved'
|
||||
import { ToastRecordingRequest } from './ToastRecordingRequest'
|
||||
import { ToastAutoMuteLargeRoom } from './ToastAutoMuteLargeRoom'
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<ToastData>
|
||||
@@ -45,6 +46,11 @@ const renderToast = (
|
||||
case NotificationType.LowerHand:
|
||||
return <ToastLowerHand key={toast.key} toast={toast} state={state} />
|
||||
|
||||
case NotificationType.AutoMuteLargeRoom:
|
||||
return (
|
||||
<ToastAutoMuteLargeRoom key={toast.key} toast={toast} state={state} />
|
||||
)
|
||||
|
||||
case NotificationType.TranscriptionStarted:
|
||||
case NotificationType.TranscriptionStopped:
|
||||
case NotificationType.TranscriptionLimitReached:
|
||||
|
||||
@@ -5,6 +5,15 @@ import type { Participant } from 'livekit-client'
|
||||
import type { NotificationPayload } from './NotificationPayload'
|
||||
import type { RecordingMode } from '@/features/recording'
|
||||
|
||||
export const notifyAutoMutedOnJoin = () => {
|
||||
toastQueue.add(
|
||||
{
|
||||
type: NotificationType.AutoMuteLargeRoom,
|
||||
},
|
||||
{ timeout: NotificationDuration.ALERT }
|
||||
)
|
||||
}
|
||||
|
||||
export const showLowerHandToast = (
|
||||
participant: Participant,
|
||||
onClose: () => void
|
||||
|
||||
@@ -68,7 +68,7 @@ export const PipView = () => {
|
||||
</ConnectionStateWrapper>
|
||||
<PipStage />
|
||||
<ReactionsToolbar adjustedCentering={false} />
|
||||
<PipControlBar showScreenShare={false} />
|
||||
<PipControlBar showScreenShare={true} />
|
||||
<PipFloatingReactions />
|
||||
<NotificationProvider bottom={30} />
|
||||
</Container>
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
|
||||
interface PipPaginationProps {
|
||||
totalPageCount: number
|
||||
currentPage: number
|
||||
nextPage: () => void
|
||||
prevPage: () => void
|
||||
}
|
||||
|
||||
export const PipPagination = ({
|
||||
totalPageCount,
|
||||
currentPage,
|
||||
nextPage,
|
||||
prevPage,
|
||||
}: PipPaginationProps) => {
|
||||
const { t } = useTranslation('rooms', { keyPrefix: 'pagination' })
|
||||
|
||||
if (totalPageCount <= 1) return null
|
||||
|
||||
return (
|
||||
<Nav aria-label={t('label')}>
|
||||
<ArrowButton
|
||||
type="button"
|
||||
onClick={prevPage}
|
||||
disabled={currentPage === 1}
|
||||
aria-label={t('previous')}
|
||||
>
|
||||
<RiArrowLeftSLine size={18} />
|
||||
</ArrowButton>
|
||||
<Counter role="status">
|
||||
{t('count', { currentPage, totalPageCount })}
|
||||
</Counter>
|
||||
<ArrowButton
|
||||
type="button"
|
||||
onClick={nextPage}
|
||||
disabled={currentPage === totalPageCount}
|
||||
aria-label={t('next')}
|
||||
>
|
||||
<RiArrowRightSLine size={18} />
|
||||
</ArrowButton>
|
||||
</Nav>
|
||||
)
|
||||
}
|
||||
|
||||
const Nav = styled('nav', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
gap: '0.25rem',
|
||||
marginTop: '1rem',
|
||||
flexShrink: 0,
|
||||
},
|
||||
})
|
||||
|
||||
const ArrowButton = styled('button', {
|
||||
base: {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
width: '1.75rem',
|
||||
height: '1.75rem',
|
||||
borderRadius: '4px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: 'white',
|
||||
backgroundColor: 'primaryDark.100',
|
||||
transition: 'opacity 0.15s, background-color 0.15s',
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: 'primaryDark.75',
|
||||
},
|
||||
'&:focus-visible': {
|
||||
outline: '2px solid',
|
||||
outlineColor: 'white',
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
'&:disabled': {
|
||||
opacity: 0.3,
|
||||
cursor: 'default',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const Counter = styled('span', {
|
||||
base: {
|
||||
fontSize: '0.75rem',
|
||||
color: 'white',
|
||||
opacity: 0.8,
|
||||
whiteSpace: 'nowrap',
|
||||
padding: '0 0.25rem',
|
||||
minWidth: '3rem',
|
||||
textAlign: 'center',
|
||||
},
|
||||
})
|
||||
@@ -1,9 +1,12 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useTracks } from '@livekit/components-react'
|
||||
import { usePagination, useTracks } from '@livekit/components-react'
|
||||
import { RoomEvent, Track } from 'livekit-client'
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { PipFocusLayout } from './PipFocusLayout'
|
||||
import { PipGridLayout } from './PipGridLayout'
|
||||
import { PipPagination } from './PipPagination'
|
||||
import { StageFrame } from './StageFrame'
|
||||
import { MAX_PIP_TILES } from '../../utils/pipGrid'
|
||||
import {
|
||||
isTrackReference,
|
||||
TrackReferenceOrPlaceholder,
|
||||
@@ -38,21 +41,38 @@ export const PipStage = () => {
|
||||
[tracks]
|
||||
)
|
||||
|
||||
// Grid mode order: screen share leads, then cameras (already ordered by
|
||||
// active speaker via the `ActiveSpeakersChanged` update above).
|
||||
const gridTracks = useMemo(
|
||||
() =>
|
||||
screenShareTrack ? [screenShareTrack, ...cameraTracks] : cameraTracks,
|
||||
[screenShareTrack, cameraTracks]
|
||||
)
|
||||
|
||||
// Cap the grid at MAX_PIP_TILES per page. `usePagination` keeps the visible
|
||||
// page visually stable (active/recent speakers stay put) via its internal
|
||||
// `useVisualStableUpdate`. Called unconditionally to respect hook rules.
|
||||
const pagination = usePagination(MAX_PIP_TILES, gridTracks)
|
||||
|
||||
if (tracks.length === 0) return null
|
||||
|
||||
/**
|
||||
* The focus layout shows one main track with one thumbnail overlay,
|
||||
* so it can only fit 2 tracks. Beyond that we switch to the grid.
|
||||
*/
|
||||
if (tracks.length > 2) {
|
||||
// Grid mode: 3+ tracks. Screen share goes first so it leads the grid.
|
||||
const gridTracks = screenShareTrack
|
||||
? [screenShareTrack, ...cameraTracks]
|
||||
: cameraTracks
|
||||
if (gridTracks.length > 2) {
|
||||
return (
|
||||
<StageFrame>
|
||||
<PipGridLayout tracks={gridTracks} />
|
||||
</StageFrame>
|
||||
<StageWrapper>
|
||||
<StageFrame>
|
||||
<PipGridLayout tracks={pagination.tracks} />
|
||||
</StageFrame>
|
||||
<PipPagination
|
||||
totalPageCount={pagination.totalPageCount}
|
||||
currentPage={pagination.currentPage}
|
||||
nextPage={pagination.nextPage}
|
||||
prevPage={pagination.prevPage}
|
||||
/>
|
||||
</StageWrapper>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -75,3 +95,12 @@ export const PipStage = () => {
|
||||
</StageFrame>
|
||||
)
|
||||
}
|
||||
|
||||
const StageWrapper = styled('div', {
|
||||
base: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ export const StageFrame = ({ children }: { children: React.ReactNode }) => {
|
||||
const Container = styled('div', {
|
||||
base: {
|
||||
position: 'relative',
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
minHeight: 0,
|
||||
marginLeft: '0.5rem',
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/**
|
||||
* Maximum number of tiles rendered per PiP page. Beyond this the grid
|
||||
* paginates so large meetings stay glanceable (active speaker priority)
|
||||
* instead of subscribing to and rendering every camera in a small window.
|
||||
*/
|
||||
export const MAX_PIP_TILES = 5
|
||||
|
||||
export type PipTilePlacement = {
|
||||
gridColumn: string
|
||||
gridRow: number
|
||||
|
||||
@@ -6,5 +6,5 @@ export const useIsMetadataCollectorEnabled = () => {
|
||||
const featureEnabled = useFeatureFlagEnabled(FeatureFlags.metadataCollector)
|
||||
const isAnalyticsEnabled = useIsAnalyticsEnabled()
|
||||
|
||||
return (featureEnabled && isAnalyticsEnabled) || true
|
||||
return !isAnalyticsEnabled || Boolean(featureEnabled)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
@@ -33,6 +33,9 @@ import { useIsMobile } from '@/utils/useIsMobile'
|
||||
import { navigateTo } from '@/navigation/navigateTo'
|
||||
import { connectionObserverStore } from '@/stores/connectionObserver'
|
||||
import { PictureInPictureConference } from '@/features/pip/components/PictureInPictureConference'
|
||||
import { notifyAutoMutedOnJoin } from '@/features/notifications/utils'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { userPreferencesStore } from '@/stores/userPreferences'
|
||||
|
||||
export const Conference = ({
|
||||
roomId,
|
||||
@@ -57,6 +60,8 @@ export const Conference = ({
|
||||
|
||||
const [isConnectionWarmedUp, setIsConnectionWarmedUp] = useState(false)
|
||||
|
||||
const userPreferencesSnap = useSnapshot(userPreferencesStore)
|
||||
|
||||
const {
|
||||
mutateAsync: createRoom,
|
||||
status: createStatus,
|
||||
@@ -173,6 +178,8 @@ export const Conference = ({
|
||||
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const hasAutoMutedRef = useRef(false)
|
||||
|
||||
/*
|
||||
* Ensure stable WebSocket connection URL. This is critical for legacy browser compatibility
|
||||
* (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs in WebSocket() constructor
|
||||
@@ -228,6 +235,19 @@ export const Conference = ({
|
||||
onError={(e) => {
|
||||
posthog.captureException(e)
|
||||
}}
|
||||
onConnected={async () => {
|
||||
if (!apiConfig) return
|
||||
if (
|
||||
userPreferencesSnap.is_auto_mute_large_room_enabled &&
|
||||
!hasAutoMutedRef.current &&
|
||||
userConfig.audioEnabled &&
|
||||
room.numParticipants > apiConfig.auto_mute_on_join_threshold
|
||||
) {
|
||||
hasAutoMutedRef.current = true
|
||||
await room.localParticipant.setMicrophoneEnabled(false)
|
||||
notifyAutoMutedOnJoin()
|
||||
}
|
||||
}}
|
||||
onDisconnected={(e) => {
|
||||
const metadata = {
|
||||
room_id: roomId,
|
||||
|
||||
@@ -216,6 +216,14 @@ export const Join = ({
|
||||
try {
|
||||
const track = await createLocalAudioTrack({
|
||||
deviceId: { exact: audioDeviceId },
|
||||
noiseSuppression: true,
|
||||
echoCancellation: true,
|
||||
autoGainControl: true,
|
||||
voiceIsolation: false,
|
||||
// Audio quality optimized for voice
|
||||
sampleRate: 48000, // High quality sample rate
|
||||
channelCount: 1, // Mono for voice calls (saves bandwidth)
|
||||
sampleSize: 16, // 16-bit audio
|
||||
})
|
||||
setDynamicAudioTrack(track)
|
||||
} catch (error) {
|
||||
|
||||
+95
-65
@@ -400,6 +400,8 @@ export const EffectsConfiguration = ({
|
||||
config: ProcessorConfig
|
||||
isSelected: boolean
|
||||
tooltip: string
|
||||
ariaLabel: string
|
||||
ariaDeleteLabel: string
|
||||
file: ApiFileItem
|
||||
}[]
|
||||
}>(() => {
|
||||
@@ -426,7 +428,9 @@ export const EffectsConfiguration = ({
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
return {
|
||||
id,
|
||||
tooltip: t(`blur.light.${selectedId === id ? 'clear' : 'apply'}`),
|
||||
tooltip: t(
|
||||
`blur.${item.key}.${selectedId === id ? 'clear' : 'apply'}`
|
||||
),
|
||||
radius: item.radius,
|
||||
isSelected: selectedId === id,
|
||||
Icon: item.icon,
|
||||
@@ -443,9 +447,11 @@ export const EffectsConfiguration = ({
|
||||
}
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
const isSelected = selectedId === id
|
||||
const prefix = isSelected ? 'selectedLabel' : 'apply'
|
||||
const backgroundName = t(`virtual.presets.descriptions.${index}`)
|
||||
const ariaLabel = `${t(`virtual.presets.${prefix}`)} ${backgroundName}`
|
||||
const ariaLabelPrefix = t(
|
||||
isSelected ? `virtual.selectedLabel` : `virtual.apply`
|
||||
)
|
||||
const ariaLabel = `${ariaLabelPrefix} ${backgroundName}`
|
||||
|
||||
return {
|
||||
tooltip: backgroundName,
|
||||
@@ -467,13 +473,22 @@ export const EffectsConfiguration = ({
|
||||
}
|
||||
|
||||
const id = deriveIdFromProcessorConfig(config)
|
||||
const isSelected = selectedId === id
|
||||
const ariaLabel = t(
|
||||
isSelected
|
||||
? `virtual.personal.selectedLabel`
|
||||
: `virtual.personal.apply`
|
||||
)
|
||||
const ariaDeleteLabel = t('virtual.personal.deleteLabel')
|
||||
|
||||
return {
|
||||
tooltip: file.title,
|
||||
id,
|
||||
config,
|
||||
isSelected: selectedId === id,
|
||||
isSelected,
|
||||
file,
|
||||
ariaLabel,
|
||||
ariaDeleteLabel,
|
||||
}
|
||||
}),
|
||||
}
|
||||
@@ -552,6 +567,7 @@ export const EffectsConfiguration = ({
|
||||
[layout === 'vertical' ? 'height' : 'minHeight']: '175px',
|
||||
borderRadius: '8px',
|
||||
}}
|
||||
aria-hidden={true}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@@ -664,7 +680,7 @@ export const EffectsConfiguration = ({
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={2}
|
||||
lvl={3}
|
||||
style={{
|
||||
marginBottom: '0.4rem',
|
||||
}}
|
||||
@@ -680,6 +696,7 @@ export const EffectsConfiguration = ({
|
||||
paddingBottom: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
role="list"
|
||||
>
|
||||
{createFileMutation.isPending &&
|
||||
fileBeingUploadedObjectUrlRef.current && (
|
||||
@@ -721,48 +738,54 @@ export const EffectsConfiguration = ({
|
||||
className={
|
||||
'hoverGroup ' + css({ position: 'relative' })
|
||||
}
|
||||
role="listitem"
|
||||
>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.tooltip}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={getHandleSelectChangeFile(option.file)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.file.url!})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${option.file.id}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<Button
|
||||
className={
|
||||
'hoverGroupChild ' +
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
})
|
||||
}
|
||||
size={'xs'}
|
||||
variant={'tertiary'}
|
||||
onClick={() => {
|
||||
if (option.isSelected) {
|
||||
// we remove the current effect
|
||||
toggleEffect(option.config)
|
||||
<div role="group" aria-label={option.file.title}>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={getHandleSelectChangeFile(
|
||||
option.file
|
||||
)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.file.url!})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-${option.file.id}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<Button
|
||||
className={
|
||||
'hoverGroupChild ' +
|
||||
css({
|
||||
position: 'absolute',
|
||||
top: '-8px',
|
||||
right: '-8px',
|
||||
transition: 'opacity 0.2s ease-in-out',
|
||||
})
|
||||
}
|
||||
deleteFileMutation.mutate({
|
||||
fileId: option.file.id,
|
||||
})
|
||||
}}
|
||||
isDisabled={deleteFileMutation.isPending}
|
||||
>
|
||||
<RiDeleteBinLine size={16} />
|
||||
</Button>
|
||||
aria-label={option.ariaDeleteLabel}
|
||||
size={'xs'}
|
||||
variant={'tertiary'}
|
||||
onClick={() => {
|
||||
if (option.isSelected) {
|
||||
// we remove the current effect
|
||||
toggleEffect(option.config)
|
||||
}
|
||||
deleteFileMutation.mutate({
|
||||
fileId: option.file.id,
|
||||
})
|
||||
}}
|
||||
isDisabled={deleteFileMutation.isPending}
|
||||
>
|
||||
<RiDeleteBinLine size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
@@ -775,9 +798,13 @@ export const EffectsConfiguration = ({
|
||||
>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={
|
||||
uploadNotPossibleSnap.imageBackgroundConfig.label
|
||||
}
|
||||
aria-label={`${t(
|
||||
deriveIdFromProcessorConfig(
|
||||
uploadNotPossibleSnap.imageBackgroundConfig
|
||||
) === selectedId
|
||||
? `virtual.selectedLabel`
|
||||
: `virtual.apply`
|
||||
)} ${uploadNotPossibleSnap.imageBackgroundConfig.label}`}
|
||||
isDisabled={
|
||||
processorOptions.isDisabled ||
|
||||
createFileMutation.isPending
|
||||
@@ -857,7 +884,7 @@ export const EffectsConfiguration = ({
|
||||
})}
|
||||
>
|
||||
<H
|
||||
lvl={2}
|
||||
lvl={3}
|
||||
style={{
|
||||
marginBottom: '0.4rem',
|
||||
}}
|
||||
@@ -872,24 +899,27 @@ export const EffectsConfiguration = ({
|
||||
paddingBottom: '0.5rem',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
role="list"
|
||||
>
|
||||
{processorOptions.virtualBackgrounds.map((option) => (
|
||||
<VisualOnlyTooltip key={option.id} tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => toggleEffect(option.config)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-preset-${option.index}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
<div role="listitem" key={option.id}>
|
||||
<VisualOnlyTooltip tooltip={option.tooltip}>
|
||||
<ToggleButton
|
||||
variant="bigSquare"
|
||||
aria-label={option.ariaLabel}
|
||||
isDisabled={processorOptions.isDisabled}
|
||||
onChange={() => toggleEffect(option.config)}
|
||||
isSelected={option.isSelected}
|
||||
className={css({
|
||||
bgSize: 'cover',
|
||||
})}
|
||||
style={{
|
||||
backgroundImage: `url(${option.thumbnailPath})`,
|
||||
}}
|
||||
data-attr={`toggle-virtual-preset-${option.index}`}
|
||||
/>
|
||||
</VisualOnlyTooltip>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
import type { 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
|
||||
|
||||
/**
|
||||
* Lazily load the WASM processor factory.
|
||||
*
|
||||
* '@libreaudio/la-call' pulls in a WebAssembly payload, so we defer importing
|
||||
* it until a processor is actually initialized rather than at module load.
|
||||
* The import promise is cached so repeated init() calls reuse the same module.
|
||||
*/
|
||||
type CreateWasmProcessor =
|
||||
(typeof import('@libreaudio/la-call'))['createWasmProcessor']
|
||||
|
||||
let wasmProcessorPromise: Promise<CreateWasmProcessor> | undefined
|
||||
|
||||
function loadWasmProcessor(): Promise<CreateWasmProcessor> {
|
||||
if (!wasmProcessorPromise) {
|
||||
wasmProcessorPromise = import('@libreaudio/la-call').then(
|
||||
(mod) => mod.createWasmProcessor
|
||||
)
|
||||
}
|
||||
return wasmProcessorPromise
|
||||
}
|
||||
|
||||
export interface AudioProcessorInterface extends TrackProcessor<Track.Kind.Audio> {
|
||||
name: string
|
||||
}
|
||||
@@ -20,7 +36,7 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
|
||||
private source?: MediaStreamTrack
|
||||
private sourceNode?: MediaStreamAudioSourceNode
|
||||
private destinationNode?: MediaStreamAudioDestinationNode
|
||||
private noiseSuppressionNode?: AudioWorkletNode
|
||||
private noiseSuppressionNode?: AudioNode
|
||||
|
||||
async init(opts: ProcessorOptions<Track.Kind.Audio>) {
|
||||
if (!opts.track) {
|
||||
@@ -35,16 +51,17 @@ export class RnnNoiseProcessor implements AudioProcessorInterface {
|
||||
await audioContext.resume()
|
||||
}
|
||||
|
||||
await audioContext.audioWorklet.addModule(NoiseSuppressorWorklet)
|
||||
|
||||
this.sourceNode = audioContext.createMediaStreamSource(
|
||||
new MediaStream([this.source])
|
||||
)
|
||||
|
||||
this.noiseSuppressionNode = new AudioWorkletNode(
|
||||
audioContext,
|
||||
NoiseSuppressorWorklet_Name
|
||||
)
|
||||
const createWasmProcessor = await loadWasmProcessor()
|
||||
this.noiseSuppressionNode = await createWasmProcessor(audioContext, {
|
||||
intensity: 90,
|
||||
})
|
||||
if (!this.noiseSuppressionNode) {
|
||||
throw new Error('Failed to create Wasm processor')
|
||||
}
|
||||
|
||||
this.destinationNode = audioContext.createMediaStreamDestination()
|
||||
|
||||
|
||||
@@ -39,6 +39,19 @@ export const GeneralTab = ({ id }: GeneralTabProps) => {
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
<Field
|
||||
type="switch"
|
||||
label={t('preferences.autoMuteLargeRoom.label')}
|
||||
description={t('preferences.autoMuteLargeRoom.description')}
|
||||
isSelected={userPreferencesSnap.is_auto_mute_large_room_enabled}
|
||||
onChange={(value) =>
|
||||
(userPreferencesStore.is_auto_mute_large_room_enabled = value)
|
||||
}
|
||||
wrapperProps={{
|
||||
noMargin: true,
|
||||
fullWidth: true,
|
||||
}}
|
||||
/>
|
||||
</TabPanel>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { VisualOnlyTooltip } from '@/primitives/VisualOnlyTooltip'
|
||||
|
||||
import { useLoginHint } from '@/hooks/useLoginHint'
|
||||
import { logout } from '@/features/auth/utils/logout'
|
||||
import { useMemo } from 'react'
|
||||
|
||||
const Logo = () => (
|
||||
<img
|
||||
@@ -84,6 +85,16 @@ const LoginHint = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const HIDE_LOGIN_PARAM = 'hideLogin'
|
||||
|
||||
const isLoginButtonHidden = () => {
|
||||
if (typeof window === 'undefined') return false
|
||||
const value = new URLSearchParams(window.location.search).get(
|
||||
HIDE_LOGIN_PARAM
|
||||
)
|
||||
return value === 'true'
|
||||
}
|
||||
|
||||
export const Header = () => {
|
||||
const { t } = useTranslation()
|
||||
const isHome = useMatchesRoute('home')
|
||||
@@ -92,6 +103,9 @@ export const Header = () => {
|
||||
const isTermsOfService = useMatchesRoute('termsOfService')
|
||||
const isRoom = useMatchesRoute('room')
|
||||
const { user, isLoggedIn } = useUser()
|
||||
|
||||
const loginButtonDisabledByUrl = useMemo(() => isLoginButtonHidden(), [])
|
||||
|
||||
const userLabel = user?.full_name || user?.email
|
||||
const loggedInTooltip = t('loggedInUserTooltip')
|
||||
const loggedInAriaLabel = userLabel
|
||||
@@ -152,7 +166,8 @@ export const Header = () => {
|
||||
!isHome &&
|
||||
!isLegalTerms &&
|
||||
!isAccessibility &&
|
||||
!isTermsOfService && (
|
||||
!isTermsOfService &&
|
||||
!loginButtonDisabledByUrl && (
|
||||
<>
|
||||
<LoginButton proConnectHint={false} />
|
||||
<LoginHint />
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Du hast begonnen zu sprechen – deine Hand wird daher automatisch gesenkt.",
|
||||
"dismiss": "Hand oben lassen"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "Sie haben an einer großen Besprechung teilgenommen. Ihr Mikrofon wurde automatisch stummgeschaltet.",
|
||||
"dismiss": "Stummschaltung aufheben"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} hat mit {{emoji}} reagiert"
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Teilnehmerseiten",
|
||||
"count": "{{currentPage}} von {{totalPageCount}}",
|
||||
"next": "nächste Seite",
|
||||
"previous": "vorherige Seite"
|
||||
@@ -290,11 +291,14 @@
|
||||
"apply": "Ersetze deinen Hintergrund:",
|
||||
"personal": {
|
||||
"title": "Meine Hintergründe",
|
||||
"selectedLabel": "Hintergrund abwählen (er ist aktuell angewendet).",
|
||||
"apply": "Als Hintergrund verwenden.",
|
||||
"selectFileTooltip": "Wähle ein Bild aus, das als persönlicher Hintergrund verwendet werden soll",
|
||||
"notLoggedInWarning": "Du bist nicht angemeldet, der persönliche Hintergrund wird nicht von einem Meeting zum anderen gespeichert.",
|
||||
"warningUploadDisabled": "Persönliche Hintergründe werden derzeit nicht von einem Meeting zum anderen gespeichert.",
|
||||
"uploadLimitReached": "Du kannst keine weiteren persönlichen Hintergründe hinzufügen.",
|
||||
"uploadInProgress": "Datei wird hochgeladen…",
|
||||
"deleteLabel": "Benutzerdefinierten Hintergrund löschen.",
|
||||
"errors": {
|
||||
"close": "Schließen",
|
||||
"file_too_large": {
|
||||
@@ -429,10 +433,10 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premium-Funktion",
|
||||
"heading": "Advanced-Funktion",
|
||||
"body": "Diese Funktion ist für Sie nicht verfügbar. Bitte wenden Sie sich an den Support, um weitere Informationen zu erhalten.",
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zum Premium-Zugang öffnen – öffnet in neuem Tab",
|
||||
"linkAriaLabel": "Dokumentation zum Advanced-Zugang öffnen – öffnet in neuem Tab",
|
||||
"dividerLabel": "ODER",
|
||||
"login": {
|
||||
"heading": "Anmeldung erforderlich",
|
||||
@@ -482,10 +486,10 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premium-Funktion",
|
||||
"heading": "Advanced-Funktion",
|
||||
"body": "Diese Funktion ist für Sie nicht verfügbar. Bitte wenden Sie sich an den Support, um weitere Informationen zu erhalten.",
|
||||
"linkMore": "Dokumentation öffnen",
|
||||
"linkAriaLabel": "Dokumentation zum Premium-Zugang öffnen – öffnet in neuem Tab",
|
||||
"linkAriaLabel": "Dokumentation zum Advanced-Zugang öffnen – öffnet in neuem Tab",
|
||||
"dividerLabel": "ODER",
|
||||
"login": {
|
||||
"heading": "Anmeldung erforderlich",
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Meetings ohne Teilnehmende verlassen",
|
||||
"description": "Meetings nach einigen Minuten automatisch verlassen, wenn keine andere Person beitritt"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Mikrofon automatisch stummschalten",
|
||||
"description": "Dein Mikrofon wird automatisch stummgeschaltet, wenn du an einer großen Besprechung teilnimmst."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "It seems you have started speaking, so your hand will be lowered.",
|
||||
"dismiss": "Keep hand raised"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "You have joined a large meeting. Your microphone has been automatically muted.",
|
||||
"dismiss": "Unmute"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reacted with {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Participants pages",
|
||||
"count": "{{currentPage}} of {{totalPageCount}}",
|
||||
"next": "next page",
|
||||
"previous": "previous page"
|
||||
@@ -290,11 +291,14 @@
|
||||
"apply": "Replace your background:",
|
||||
"personal": {
|
||||
"title": "My backgrounds",
|
||||
"selectedLabel": "Unselect background (it's currently applied).",
|
||||
"apply": "Use as background.",
|
||||
"selectFileTooltip": "Select an image file to use as a personal background",
|
||||
"notLoggedInWarning": "You are not logged-in, personal backgrounds won't be saved from one meeting to the other.",
|
||||
"warningUploadDisabled": "Personal backgrounds are currently not saved from one meeting to the other.",
|
||||
"uploadLimitReached": "You cannot upload more personal backgrounds.",
|
||||
"uploadInProgress": "Image is being uploaded…",
|
||||
"deleteLabel": "Delete background.",
|
||||
"errors": {
|
||||
"close": "Close",
|
||||
"file_too_large": {
|
||||
@@ -428,10 +432,10 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premium feature",
|
||||
"heading": "Advanced feature",
|
||||
"body": "This feature is not available to you. Please contact support for more information.",
|
||||
"linkMore": "Open documentation",
|
||||
"linkAriaLabel": "Open documentation about premium access - opens in new window",
|
||||
"linkAriaLabel": "Open documentation about advanced access - opens in new window",
|
||||
"dividerLabel": "OR",
|
||||
"login": {
|
||||
"heading": "You are not logged in!",
|
||||
@@ -481,10 +485,10 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premium feature",
|
||||
"heading": "Advanced feature",
|
||||
"body": "This feature is not available to you. Please contact support for more information.",
|
||||
"linkMore": "Open documentation",
|
||||
"linkAriaLabel": "Open documentation about premium access - opens in new window",
|
||||
"linkAriaLabel": "Open documentation about advanced access - opens in new window",
|
||||
"dividerLabel": "OR",
|
||||
"login": {
|
||||
"heading": "You are not logged in!",
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Leave calls with no participants",
|
||||
"description": "Automatically leaves a call after a few minutes if no other participant joins"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Automatically mute my microphone",
|
||||
"description": "Automatically mute your microphone when joining a large meeting."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
@@ -21,8 +25,8 @@
|
||||
"disabled": "Microphone disabled"
|
||||
},
|
||||
"noiseReduction": {
|
||||
"label": "Noise reduction",
|
||||
"heading": "Noise reduction",
|
||||
"label": "Advanced voice optimization",
|
||||
"heading": "Audio filters",
|
||||
"ariaLabel": {
|
||||
"enable": "Enable noise reduction",
|
||||
"disable": "Disable noise reduction"
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Il semblerait que vous ayez pris la parole, donc la main va être baissée.",
|
||||
"dismiss": "Laisser la main levée"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "Vous venez de rejoindre une grande réunion. Votre micro a été automatiquement coupé.",
|
||||
"dismiss": "Rétablir le micro"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} a reagi avec {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Pages des participants",
|
||||
"count": "{{currentPage}} sur {{totalPageCount}}",
|
||||
"next": "page suivante",
|
||||
"previous": "page précédente"
|
||||
@@ -290,11 +291,14 @@
|
||||
"apply": "Remplacer votre arrière plan :",
|
||||
"personal": {
|
||||
"title": "Mes arrière-plans",
|
||||
"selectedLabel": "Ne plus utiliser comme arrière-plan (actuellement actif).",
|
||||
"apply": "Utiliser comme arrière plan.",
|
||||
"selectFileTooltip": "Sélectionnez une image à utiliser comme arrière-plan",
|
||||
"notLoggedInWarning": "Vous n'êtes pas connecté, l'arrière-plan personnel ne sera pas sauvegardé d'une réunion à l'autre.",
|
||||
"warningUploadDisabled": "Les arrière-plans personnels ne sont actuellement pas sauvegardés d'une réunion à l'autre.",
|
||||
"uploadLimitReached": "Vous ne pouvez pas ajouter plus d'arrière-plans personnels.",
|
||||
"uploadInProgress": "Image en cours d'envoi…",
|
||||
"deleteLabel": "Supprimer l'arrière-plan.",
|
||||
"errors": {
|
||||
"close": "Fermer",
|
||||
"file_too_large": {
|
||||
@@ -428,7 +432,7 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Fonctionnalité premium",
|
||||
"heading": "Fonctionnalité avancée",
|
||||
"body": "Cette fonctionnalité ne vous est pas ouverte. Contactez le support pour obtenir plus d'informations.",
|
||||
"linkMore": "Ouvrir la documentation",
|
||||
"dividerLabel": "OU",
|
||||
@@ -441,7 +445,7 @@
|
||||
"body": "L'hôte recevra une notification et pourra démarrer la transcription pour vous.",
|
||||
"buttonLabel": "Demander"
|
||||
},
|
||||
"linkAriaLabel": "Ouvrir la documentation sur l'accès premium - ouvre dans une nouvelle fenêtre"
|
||||
"linkAriaLabel": "Ouvrir la documentation sur la fonctionnalité avancée - ouvre dans une nouvelle fenêtre"
|
||||
}
|
||||
},
|
||||
"screenRecording": {
|
||||
@@ -481,7 +485,7 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Fonctionnalité premium",
|
||||
"heading": "Fonctionnalité avancée",
|
||||
"body": "Cette fonctionnalité ne vous est pas ouverte. Contactez le support pour obtenir plus d'informations.",
|
||||
"linkMore": "Ouvrir la documentation",
|
||||
"dividerLabel": "OU",
|
||||
@@ -494,7 +498,7 @@
|
||||
"body": "L'hôte recevra une notification et pourra démarrer l'enregistrement pour vous.",
|
||||
"buttonLabel": "Demander"
|
||||
},
|
||||
"linkAriaLabel": "Ouvrir la documentation sur l'accès premium - ouvre dans une nouvelle fenêtre"
|
||||
"linkAriaLabel": "Ouvrir la documentation sur la fonctionnalité avancée - ouvre dans une nouvelle fenêtre"
|
||||
},
|
||||
"durationMessage": "(limité à {{max_duration}}) "
|
||||
},
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Quitter les appels sans autre participant",
|
||||
"description": "Vous fait quitter un appel au bout de quelques minutes si aucun autre participant ne vous rejoint"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Couper automatiquement mon micro",
|
||||
"description": "Coupe automatiquement votre micro lorsque vous rejoignez une réunion de grande taille."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -14,6 +14,10 @@
|
||||
"auto": "Het lijkt erop dat u bent begonnen te spreken, dus we laten uw hand zakken.",
|
||||
"dismiss": "Houdt uw hand opgestoken"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"auto": "U bent zojuist lid geworden van een grote vergadering. Uw microfoon is automatisch gedempt.",
|
||||
"dismiss": "Microfoon inschakelen"
|
||||
},
|
||||
"reaction": {
|
||||
"description": "{{name}} reageerde met {{emoji}}"
|
||||
},
|
||||
|
||||
@@ -97,6 +97,7 @@
|
||||
}
|
||||
},
|
||||
"pagination": {
|
||||
"label": "Pagina's van deelnemers",
|
||||
"count": "{{currentPage}} van {{totalPageCount}}",
|
||||
"next": "volgende pagina",
|
||||
"previous": "vorige pagina"
|
||||
@@ -290,11 +291,14 @@
|
||||
"apply": "Vervang je achtergrond:",
|
||||
"personal": {
|
||||
"title": "Mijn achtergronden",
|
||||
"selectedLabel": "Deselecteer de achtergrond (deze is momenteel toegepast).",
|
||||
"apply": "Gebruik als achtergrond.",
|
||||
"selectFileTooltip": "Selecteer een afbeeldingsbestand om te gebruiken als persoonlijke achtergrond",
|
||||
"notLoggedInWarning": "U bent niet ingelogd, persoonlijke achtergronden worden niet opgeslagen van de ene vergadering naar de andere.",
|
||||
"warningUploadDisabled": "Persoonlijke achtergronden worden momenteel niet opgeslagen van de ene vergadering naar de andere.",
|
||||
"uploadLimitReached": "U kunt geen persoonlijke achtergronden meer uploaden.",
|
||||
"uploadInProgress": "Afbeelding wordt geüpload…",
|
||||
"deleteLabel": "Aangepaste achtergrond verwijderen.",
|
||||
"errors": {
|
||||
"close": "Sluiten",
|
||||
"file_too_large": {
|
||||
@@ -428,10 +432,10 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premiumfunctie",
|
||||
"heading": "Geavanceerde functie",
|
||||
"body": "Deze functie is niet voor u beschikbaar. Neem contact op met de ondersteuning voor meer informatie.",
|
||||
"linkMore": "Documentatie openen",
|
||||
"linkAriaLabel": "Documentatie over premiumtoegang openen - opent in nieuw venster",
|
||||
"linkAriaLabel": "Documentatie over Geavanceerde functie openen - opent in nieuw venster",
|
||||
"dividerLabel": "OF",
|
||||
"login": {
|
||||
"heading": "Inloggen vereist",
|
||||
@@ -481,10 +485,10 @@
|
||||
}
|
||||
},
|
||||
"premium": {
|
||||
"heading": "Premiumfunctie",
|
||||
"heading": "Geavanceerde functie",
|
||||
"body": "Deze functie is niet voor u beschikbaar. Neem contact op met de ondersteuning voor meer informatie.",
|
||||
"linkMore": "Documentatie openen",
|
||||
"linkAriaLabel": "Documentatie over premiumtoegang openen - opent in nieuw venster",
|
||||
"linkAriaLabel": "Documentatie over Geavanceerde functie openen - opent in nieuw venster",
|
||||
"dividerLabel": "OF",
|
||||
"login": {
|
||||
"heading": "Inloggen vereist",
|
||||
|
||||
@@ -12,6 +12,10 @@
|
||||
"idleDisconnectModal": {
|
||||
"label": "Verlaat oproepen zonder deelnemers",
|
||||
"description": "Verlaat automatisch een oproep na een paar minuten als geen andere deelnemer meedoet"
|
||||
},
|
||||
"autoMuteLargeRoom": {
|
||||
"label": "Mijn microfoon automatisch dempen",
|
||||
"description": "Uw microfoon wordt automatisch gedempt wanneer u deelneemt aan een grote vergadering."
|
||||
}
|
||||
},
|
||||
"audio": {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { STORAGE_KEYS } from '@/utils/storageKeys'
|
||||
|
||||
type State = {
|
||||
is_idle_disconnect_modal_enabled: boolean
|
||||
is_auto_mute_large_room_enabled: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_STATE = {
|
||||
is_idle_disconnect_modal_enabled: true,
|
||||
is_auto_mute_large_room_enabled: true,
|
||||
}
|
||||
|
||||
function getUserPreferencesState(): State {
|
||||
|
||||
@@ -37,7 +37,7 @@ _summaryEnvVars: &summaryEnvVars
|
||||
secretKeyRef:
|
||||
name: secret-dev
|
||||
key: WHISPERX_BASE_URL
|
||||
WHISPERX_ASR_MODEL: large-v2
|
||||
WHISPERX_ASR_MODEL: openai/whisper-large-v3
|
||||
WHISPERX_DEFAULT_LANGUAGE: fr
|
||||
LLM_BASE_URL:
|
||||
secretKeyRef:
|
||||
@@ -53,6 +53,12 @@ _summaryEnvVars: &summaryEnvVars
|
||||
CELERY_BROKER_URL: redis://default:pass@redis-master:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://default:pass@redis-master:6379/1
|
||||
TASK_TRACKER_REDIS_URL: redis://default:pass@redis-master:6379/1
|
||||
IS_RESOLVE_SPEAKER_IDENTITIES_ENABLED: true
|
||||
RESOLVE_SPEAKER_IDENTITIES_DEFAULT_OVERLAP: 0.5
|
||||
RESOLVE_SPEAKER_ENABLE_SPLIT_ON_WORDS: true
|
||||
RESOLVE_SPEAKER_MAX_WORD_DURATION: 1
|
||||
POSTHOG_API_KEY: your-posthog-key
|
||||
POSTHOG_ENABLED: False
|
||||
|
||||
_summaryImage: &summaryImage
|
||||
repository: localhost:5001/meet-summary
|
||||
|
||||
@@ -23,6 +23,9 @@ frontend:
|
||||
- name: outlook-addon-manifest
|
||||
configMap:
|
||||
name: outlook-addon-manifest
|
||||
- name: frontend-nginx-config
|
||||
configMap:
|
||||
name: frontend-nginx-config
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: outlook-addon-config
|
||||
@@ -33,9 +36,18 @@ frontend:
|
||||
mountPath: /usr/share/nginx/html/addons/outlook/manifest.xml
|
||||
subPath: manifest.xml
|
||||
readOnly: true
|
||||
- name: frontend-nginx-config
|
||||
mountPath: /etc/nginx/conf.d/default.conf
|
||||
subPath: default.conf
|
||||
readOnly: true
|
||||
|
||||
outlookAddon:
|
||||
enabled: true
|
||||
baseUrl: "https://meet.127.0.0.1.nip.io"
|
||||
enableSourceTracking: true
|
||||
feedbackForm: "https://tests.com/"
|
||||
appName: "Visio"
|
||||
id: "a025f0f6-757a-4790-97f3-99c66c4a5795"
|
||||
|
||||
frontendNginxConfig:
|
||||
enabled: true
|
||||
|
||||
@@ -28,6 +28,7 @@ agentSubtitles:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
ENABLE_SILERO_VAD: "false"
|
||||
STT_PROVIDER: kyutai
|
||||
|
||||
image:
|
||||
repository: localhost:5001/meet-agents
|
||||
@@ -53,3 +54,6 @@ agentSubtitles:
|
||||
items:
|
||||
- key: cacert.pem
|
||||
path: cert.pem
|
||||
|
||||
frontendNginxConfig:
|
||||
enabled: false
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
{{- if .Values.frontendNginxConfig.enabled }}
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: frontend-nginx-config
|
||||
namespace: {{ .Release.Namespace }}
|
||||
data:
|
||||
default.conf: |
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
server_tokens off;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = /.well-known/windows-app-web-link {
|
||||
default_type application/json;
|
||||
alias /usr/share/nginx/html/.well-known/windows-app-web-link;
|
||||
add_header Content-Disposition "attachment; filename=windows-app-web-link";
|
||||
}
|
||||
|
||||
# Manifest — fetched, never iframed
|
||||
location = /addons/outlook/manifest.xml {
|
||||
alias /usr/share/nginx/html/addons/outlook/manifest.xml;
|
||||
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header X-Frame-Options "DENY";
|
||||
add_header Content-Security-Policy "frame-ancestors 'none'";
|
||||
}
|
||||
|
||||
location = /addons/outlook/assets/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~* ^/addons/outlook/assets/(.+\.(?:css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot))/?$ {
|
||||
root /usr/share/nginx/html;
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000, immutable" always;
|
||||
add_header Access-Control-Allow-Origin "*";
|
||||
add_header Vary "Origin" always;
|
||||
}
|
||||
|
||||
location = /addons/outlook/ {
|
||||
return 404;
|
||||
}
|
||||
|
||||
location ~ ^/addons/outlook(/.*)?$ {
|
||||
alias /usr/share/nginx/html/addons/outlook$1;
|
||||
error_page 404 =200 /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache" always;
|
||||
add_header Expires 0 always;
|
||||
|
||||
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
|
||||
|
||||
set $nonce $request_id;
|
||||
|
||||
set $csp "default-src 'self'; upgrade-insecure-requests; ";
|
||||
set $csp "${csp}frame-ancestors ${ms_domains}; ";
|
||||
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
|
||||
set $csp "${csp}style-src 'self' 'unsafe-inline'; ";
|
||||
set $csp "${csp}img-src 'self' data:; ";
|
||||
set $csp "${csp}font-src 'self' data:; ";
|
||||
set $csp "${csp}connect-src 'self' ${ms_domains}; ";
|
||||
set $csp "${csp}frame-src 'none'; ";
|
||||
set $csp "${csp}object-src 'none'; ";
|
||||
set $csp "${csp}base-uri 'none'; ";
|
||||
|
||||
add_header Content-Security-Policy $csp;
|
||||
|
||||
sub_filter 'NONCE_PLACEHOLDER' $nonce;
|
||||
sub_filter_once off;
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
# Add no-cache headers
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache"; # HTTP 1.0 header for backward compatibility
|
||||
add_header Expires 0;
|
||||
}
|
||||
|
||||
# Optionally, handle 404 errors by redirecting to index.html
|
||||
error_page 404 =200 /index.html;
|
||||
}
|
||||
{{- end }}
|
||||
@@ -1,4 +1,4 @@
|
||||
apiVersion: v2
|
||||
type: application
|
||||
name: meet
|
||||
version: 0.0.22
|
||||
version: 0.0.25
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<OfficeApp xmlns="http://schemas.microsoft.com/office/appforoffice/1.1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bt="http://schemas.microsoft.com/office/officeappbasictypes/1.0" xmlns:mailappor="http://schemas.microsoft.com/office/mailappversionoverrides/1.0" xsi:type="MailApp">
|
||||
<Id>{{ .id }}</Id>
|
||||
<Version>0.0.1.0</Version>
|
||||
<Version>0.0.2.0</Version>
|
||||
<ProviderName>{{ .appName }}</ProviderName>
|
||||
<DefaultLocale>fr-FR</DefaultLocale>
|
||||
<DisplayName DefaultValue="{{ .appName }}"/>
|
||||
@@ -87,9 +87,9 @@
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Add.16x16"/>
|
||||
<bt:Image size="32" resid="Add.32x32"/>
|
||||
<bt:Image size="80" resid="Add.80x80"/>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromMail</FunctionName>
|
||||
@@ -126,9 +126,9 @@
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Add.16x16"/>
|
||||
<bt:Image size="32" resid="Add.32x32"/>
|
||||
<bt:Image size="80" resid="Add.80x80"/>
|
||||
<bt:Image size="16" resid="Icon.16x16"/>
|
||||
<bt:Image size="32" resid="Icon.32x32"/>
|
||||
<bt:Image size="80" resid="Icon.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ExecuteFunction">
|
||||
<FunctionName>generateMeetingLinkFromCalendar</FunctionName>
|
||||
@@ -175,15 +175,34 @@
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="{{ .baseUrl }}/taskpane.html"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<!-- Default (French) -->
|
||||
<bt:String id="GroupLabel" DefaultValue="{{ .appName }}"/>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien {{ .appName }}"/>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres"/>
|
||||
<bt:String id="GenerateLink.Label" DefaultValue="Ajouter un lien {{ .appName }}">
|
||||
<bt:Override Locale="en-US" Value="Add a {{ .appName }} link"/>
|
||||
<bt:Override Locale="de-DE" Value="{{ .appName }}-Link hinzufügen"/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Label" DefaultValue="Ouvrir les paramètres">
|
||||
<bt:Override Locale="en-US" Value="Open settings"/>
|
||||
<bt:Override Locale="de-DE" Value="Einstellungen öffnen"/>
|
||||
</bt:String>
|
||||
<bt:String id="OpenSettings.Label" DefaultValue="Paramètres">
|
||||
<bt:Override Locale="en-US" Value="Settings"/>
|
||||
<bt:Override Locale="de-DE" Value="Einstellungen"/>
|
||||
</bt:String>
|
||||
</bt:ShortStrings>
|
||||
<bt:LongStrings>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}."/>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion {{ .appName }} et l'insère dans l'événement."/>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}."/>
|
||||
<bt:String id="GenerateLink.Tooltip" DefaultValue="Génère un lien de réunion {{ .appName }} et l'insère dans l'événement.">
|
||||
<bt:Override Locale="de-DE" Value="Generiert einen {{ .appName }}-Besprechungslink und fügt ihn in den Termin ein."/>
|
||||
<bt:Override Locale="en-US" Value="Generates a {{ .appName }} meeting link and inserts it into the item."/>
|
||||
</bt:String>
|
||||
<bt:String id="TaskpaneButton.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}.">
|
||||
<bt:Override Locale="de-DE" Value="Öffnet die {{ .appName }}-Verbindungseinstellungen."/>
|
||||
<bt:Override Locale="en-US" Value="Opens the {{ .appName }} connection settings."/>
|
||||
</bt:String>
|
||||
<bt:String id="OpenSettings.Tooltip" DefaultValue="Ouvre les paramètres de connexion {{ .appName }}.">
|
||||
<bt:Override Locale="de-DE" Value="Öffnet die {{ .appName }}-Verbindungseinstellungen."/>
|
||||
<bt:Override Locale="en-US" Value="Opens the {{ .appName }} connection settings."/>
|
||||
</bt:String>
|
||||
</bt:LongStrings>
|
||||
</Resources>
|
||||
</VersionOverrides>
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
{{- if .Values.backend.cronjobs -}}
|
||||
{{- $envVars := include "meet.common.env" (list . .Values.backend) -}}
|
||||
{{- $fullName := include "meet.backend.fullname" . -}}
|
||||
{{- $component := "backend-cronjob-list" -}}
|
||||
apiVersion: batch/v1
|
||||
kind: CronJobList
|
||||
items:
|
||||
{{- range .Values.backend.cronjobs }}
|
||||
- apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: {{ $fullName }}-{{ .name }}
|
||||
namespace: {{ $.Release.Namespace | quote }}
|
||||
labels:
|
||||
{{- include "meet.common.labels" (list $ $component) | nindent 8 }}
|
||||
spec:
|
||||
schedule: "{{ .schedule }}"
|
||||
concurrencyPolicy: {{ .concurrencyPolicy | default "Forbid" }}
|
||||
successfulJobsHistoryLimit: {{ .successfulJobsHistoryLimit | default 3 }}
|
||||
failedJobsHistoryLimit: {{ .failedJobsHistoryLimit | default 1 }}
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
name: {{ $fullName }}-{{ .name }}
|
||||
labels:
|
||||
cronjob-name: {{ .name }}
|
||||
{{- include "meet.common.labels" (list $ $component) | nindent 16 }}
|
||||
spec:
|
||||
{{- if $.Values.image.credentials }}
|
||||
imagePullSecrets:
|
||||
- name: {{ include "meet.secret.dockerconfigjson.name" (dict "fullname" (include "meet.fullname" $) "imageCredentials" $.Values.image.credentials) }}
|
||||
{{- end}}
|
||||
shareProcessNamespace: {{ $.Values.backend.shareProcessNamespace }}
|
||||
containers:
|
||||
{{- with $.Values.backend.sidecars }}
|
||||
{{- toYaml . | nindent 18 }}
|
||||
{{- end }}
|
||||
- name: {{ $.Chart.Name }}
|
||||
image: "{{ ($.Values.backend.image | default dict).repository | default $.Values.image.repository }}:{{ ($.Values.backend.image | default dict).tag | default $.Values.image.tag }}"
|
||||
imagePullPolicy: {{ ($.Values.backend.image | default dict).pullPolicy | default $.Values.image.pullPolicy }}
|
||||
args:
|
||||
{{- toYaml .command | nindent 22 }}
|
||||
env:
|
||||
{{- if $envVars}}
|
||||
{{- $envVars | indent 22 }}
|
||||
{{- end }}
|
||||
{{- with $.Values.backend.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml . | nindent 22 }}
|
||||
{{- end }}
|
||||
{{- with $.Values.backend.resources }}
|
||||
resources:
|
||||
{{- toYaml . | nindent 22 }}
|
||||
{{- end }}
|
||||
volumeMounts:
|
||||
{{- range $index, $value := $.Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
mountPath: {{ $value.path }}
|
||||
subPath: content
|
||||
{{- end }}
|
||||
{{- range $name, $volume := $.Values.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
mountPath: "{{ $volume.mountPath }}"
|
||||
{{- end }}
|
||||
{{- range $.Values.backend.extraVolumeMounts }}
|
||||
- name: {{ .name }}
|
||||
mountPath: {{ .mountPath }}
|
||||
subPath: {{ .subPath | default "" }}
|
||||
readOnly: {{ .readOnly }}
|
||||
{{- end }}
|
||||
restartPolicy: {{ .restartPolicy | default "Never" }}
|
||||
volumes:
|
||||
{{- range $index, $value := $.Values.mountFiles }}
|
||||
- name: "files-{{ $index }}"
|
||||
configMap:
|
||||
name: "{{ include "meet.fullname" $ }}-files-{{ $index }}"
|
||||
{{- end }}
|
||||
{{- range $name, $volume := $.Values.backend.persistence }}
|
||||
- name: "{{ $name }}"
|
||||
{{- if eq $volume.type "emptyDir" }}
|
||||
emptyDir: {}
|
||||
{{- else }}
|
||||
persistentVolumeClaim:
|
||||
claimName: "{{ $fullName }}-{{ $name }}"
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- range $.Values.backend.extraVolumes }}
|
||||
- name: {{ .name }}
|
||||
{{- if .existingClaim }}
|
||||
persistentVolumeClaim:
|
||||
claimName: {{ .existingClaim }}
|
||||
{{- else if .hostPath }}
|
||||
hostPath:
|
||||
{{ toYaml .hostPath | nindent 22 }}
|
||||
{{- else if .csi }}
|
||||
csi:
|
||||
{{- toYaml .csi | nindent 22 }}
|
||||
{{- else if .configMap }}
|
||||
configMap:
|
||||
{{- toYaml .configMap | nindent 22 }}
|
||||
{{- else if .emptyDir }}
|
||||
emptyDir:
|
||||
{{- toYaml .emptyDir | nindent 22 }}
|
||||
{{- else }}
|
||||
emptyDir: {}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -8,6 +8,8 @@ data:
|
||||
config.js: |
|
||||
window.__APP_CONFIG__ = {
|
||||
BASE_URL: {{ .Values.frontend.outlookAddon.baseUrl | quote }},
|
||||
APP_NAME: {{ .Values.frontend.outlookAddon.appName | quote }}
|
||||
APP_NAME: {{ .Values.frontend.outlookAddon.appName | quote }},
|
||||
ENABLE_SOURCE_TRACKING: {{ .Values.frontend.outlookAddon.enableSourceTracking | quote }},
|
||||
FEEDBACK_FORM: {{ .Values.frontend.outlookAddon.feedbackForm | quote }}
|
||||
};
|
||||
{{- end }}
|
||||
|
||||
@@ -279,6 +279,31 @@ backend:
|
||||
python manage.py createsuperuser --email $DJANGO_SUPERUSER_EMAIL --password $DJANGO_SUPERUSER_PASSWORD
|
||||
restartPolicy: Never
|
||||
|
||||
## @section Jobs
|
||||
|
||||
## Schedule jobs at fixed times (like cron)
|
||||
## Each cron job can be configured with name, schedule time and command.
|
||||
## @param backend.cronjobs[0].name Name of the CronJob
|
||||
## @param backend.cronjobs[0].schedule Schedule in cron format
|
||||
## @param backend.cronjobs[0].command The bash command to execute in the CronJob
|
||||
## @param backend.cronjobs[1].name Name of the CronJob
|
||||
## @param backend.cronjobs[1].schedule Schedule in cron format
|
||||
## @param backend.cronjobs[1].command The bash command to execute in the CronJob
|
||||
|
||||
cronjobs:
|
||||
- name: clean-pending-files
|
||||
schedule: "30 0 * * *"
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- "python manage.py clean_pending_files"
|
||||
- name: purge-deleted-files
|
||||
schedule: "45 0 * * *"
|
||||
command:
|
||||
- "/bin/sh"
|
||||
- "-c"
|
||||
- "python manage.py purge_deleted_files"
|
||||
|
||||
## @param backend.mergeDuplicateUsers.command backend merge_duplicate_users command
|
||||
## @param backend.mergeDuplicateUsers.restartPolicy backend merge_duplicate_users job restart policy
|
||||
mergeDuplicateUsers:
|
||||
@@ -448,12 +473,17 @@ frontend:
|
||||
## @param frontend.outlookAddon.enabled Enable the Outlook add-in ConfigMap (config.js + manifest.xml)
|
||||
## @param frontend.outlookAddon.appName Display name used in the Outlook add-in (manifest + runtime config)
|
||||
## @param frontend.outlookAddon.baseUrl Public base URL where the add-in is served (no trailing slash)
|
||||
## @param frontend.outlookAddon.enableSourceTracking Enable appending ?from=outlook-addon to the URL
|
||||
## @param frontend.outlookAddon.feedbackForm Add a link to the footer opening a feedback form
|
||||
outlookAddon:
|
||||
enabled: false
|
||||
appName: ""
|
||||
baseUrl: ""
|
||||
enableSourceTracking: false
|
||||
feedbackForm: ""
|
||||
id: ""
|
||||
|
||||
|
||||
## @section posthog
|
||||
|
||||
posthog:
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mail_mjml",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@html-to/text-cli": "0.5.4",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mail_mjml",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"description": "An util to generate html and text django's templates from mjml templates",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sdk",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"license": "ISC",
|
||||
"workspaces": [
|
||||
"./library",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sdk",
|
||||
"version": "1.19.0",
|
||||
"version": "1.21.0",
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"description": "",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
[project]
|
||||
name = "summary"
|
||||
version = "1.19.0"
|
||||
version = "1.21.0"
|
||||
dependencies = [
|
||||
"fastapi[standard]>=0.105.0",
|
||||
"uvicorn>=0.24.0",
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from urllib.parse import urljoin
|
||||
|
||||
import openai
|
||||
import requests
|
||||
import sentry_sdk
|
||||
from celery import Celery, signals
|
||||
from celery.utils.log import get_task_logger
|
||||
from openai.types.audio import Transcription
|
||||
from requests import exceptions
|
||||
|
||||
from summary.core.analytics import MetadataManager, get_analytics
|
||||
@@ -101,11 +104,6 @@ def transcribe_audio(
|
||||
)
|
||||
|
||||
logger.info("Initiating WhisperX client")
|
||||
whisperx_client = openai.OpenAI(
|
||||
api_key=settings.whisperx_api_key.get_secret_value(),
|
||||
base_url=settings.whisperx_base_url,
|
||||
max_retries=settings.whisperx_max_retries,
|
||||
)
|
||||
|
||||
# Transcription
|
||||
try:
|
||||
@@ -130,8 +128,54 @@ def transcribe_audio(
|
||||
|
||||
# Call remote service for transcription
|
||||
transcription_start_time = time.time()
|
||||
transcription = whisperx_client.audio.transcriptions.create(
|
||||
model=settings.whisperx_asr_model, file=audio_file, language=language
|
||||
|
||||
api_key = settings.whisperx_api_key.get_secret_value()
|
||||
base_url = settings.whisperx_base_url
|
||||
|
||||
# We use a manual call to the transcripion endpoint, and we do not
|
||||
# directly use the OpenAI lib for this.
|
||||
# This is because, depending on the requested response format,
|
||||
# the OpenAI lib will cast the response to a different dataclass,
|
||||
# which can result in stripping out keys & data that we are interested in.
|
||||
# This is in particular true for word_segments and words.
|
||||
# WhisperX response is slightly different from OpenAI STT endpoints
|
||||
# response.
|
||||
# At the same time "diarized_json" should be the value
|
||||
# provided to STT endpoints in our context.
|
||||
url = urljoin(base_url.rstrip("/") + "/", "audio/transcriptions")
|
||||
res = requests.post(
|
||||
url,
|
||||
data={
|
||||
"model": settings.whisperx_asr_model,
|
||||
"language": language,
|
||||
"timestamp_granularities": ["word", "segment"],
|
||||
"response_format": "diarized_json",
|
||||
},
|
||||
files={"file": audio_file},
|
||||
headers={"Authorization": f"Bearer {api_key}"},
|
||||
# Mimic OpenAI's timeout settings
|
||||
timeout=(60, 10 * 60),
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.exceptions.HTTPError as e:
|
||||
raise RuntimeError("WhisperX transcription failed, %s", res.text) from e
|
||||
|
||||
transcription_json: dict[str, Any] = res.json()
|
||||
# We remove the "usage" key from the transcription_json dictionary
|
||||
# as it may cause issues with parsing inside the Transcription model
|
||||
# Some API don't share the exact same structure for the "usage" key
|
||||
transcription_json.pop("usage", None)
|
||||
|
||||
# We force the use of the Transcription model here
|
||||
# to avoid changing too much code for now.
|
||||
# Note that it should be WhisperXResponse instead.
|
||||
transcription = Transcription.model_validate(
|
||||
# We add a dummy "text" to make the model validate,
|
||||
# Some API responses lack the "text" key.
|
||||
{"text": "", **transcription_json},
|
||||
extra="allow",
|
||||
strict=False,
|
||||
)
|
||||
|
||||
# Logging
|
||||
@@ -233,6 +277,7 @@ def format_transcript(
|
||||
recording_datetime: str | None,
|
||||
owner_timezone: str | None,
|
||||
download_link: str | None,
|
||||
form_link: str | None,
|
||||
) -> tuple[str, str]:
|
||||
"""Format a transcription into readable content with a title.
|
||||
|
||||
@@ -250,6 +295,7 @@ def format_transcript(
|
||||
recording_datetime=recording_datetime,
|
||||
owner_timezone=owner_timezone,
|
||||
download_link=download_link,
|
||||
form_link=form_link,
|
||||
)
|
||||
|
||||
|
||||
@@ -346,6 +392,13 @@ def process_audio_transcribe_summarize_v2(
|
||||
task_id,
|
||||
)
|
||||
|
||||
form_base_url = settings.transcription_satisfaction_form_base_url
|
||||
form_link = (
|
||||
f"{form_base_url}?room_id={room}"
|
||||
if (form_base_url and metadata_filename is not None)
|
||||
else None
|
||||
)
|
||||
|
||||
# Format output
|
||||
content, title = format_transcript(
|
||||
transcription,
|
||||
@@ -355,6 +408,7 @@ def process_audio_transcribe_summarize_v2(
|
||||
recording_start_at,
|
||||
owner_timezone,
|
||||
download_link,
|
||||
form_link,
|
||||
)
|
||||
|
||||
submit_content(content, title, email, sub)
|
||||
|
||||
@@ -92,7 +92,6 @@ class Settings(BaseSettings):
|
||||
whisperx_api_key: SecretStr
|
||||
whisperx_base_url: str = "https://api.openai.com/v1"
|
||||
whisperx_asr_model: str = "whisper-1"
|
||||
whisperx_max_retries: int = 0
|
||||
# ISO 639-1 language code (e.g., "en", "fr", "es")
|
||||
whisperx_default_language: Optional[str] = None
|
||||
whisperx_allowed_languages: Set[str] = {"en", "fr", "de", "nl"}
|
||||
@@ -122,6 +121,7 @@ class Settings(BaseSettings):
|
||||
|
||||
# Summary related settings
|
||||
is_summary_enabled: bool = True
|
||||
transcription_satisfaction_form_base_url: Optional[str] = None
|
||||
|
||||
# Sentry
|
||||
sentry_is_enabled: bool = False
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user