Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a357fb04d5 | |||
| e2a3b286ca | |||
| 668f093063 | |||
| f3476950c3 | |||
| 1aa8bc822d | |||
| 3698ac09eb |
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: 🐛 Bug Report
|
||||
about: If something is not working as expected 🤔.
|
||||
|
||||
---
|
||||
|
||||
## Bug Report
|
||||
|
||||
**Problematic behavior**
|
||||
A clear and concise description of the behavior.
|
||||
|
||||
**Expected behavior/code**
|
||||
A clear and concise description of what you expected to happen (or code).
|
||||
|
||||
**Steps to Reproduce**
|
||||
1. Do this...
|
||||
2. Then this...
|
||||
3. And then the bug happens!
|
||||
|
||||
**Environment**
|
||||
- Meet version:
|
||||
- Platform:
|
||||
|
||||
**Possible Solution**
|
||||
<!--- Only if you have suggestions on a fix for the bug -->
|
||||
|
||||
**Additional context/Screenshots**
|
||||
Add any other context about the problem here. If applicable, add screenshots to help explain.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
name: ✨ Feature Request
|
||||
about: I have a suggestion (and may want to build it 💪)!
|
||||
|
||||
---
|
||||
|
||||
## Feature Request
|
||||
|
||||
**Is your feature request related to a problem or unsupported use case? Please describe.**
|
||||
A clear and concise description of what the problem is. For example: I need to do some task and I have an issue...
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen. Add any considered drawbacks.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Discovery, Documentation, Adoption, Migration Strategy**
|
||||
If you can, explain how users will be able to use this and possibly write out a version the docs (if applicable).
|
||||
Maybe a screenshot or design?
|
||||
|
||||
**Do you want to work on it through a Pull Request?**
|
||||
<!-- Make sure to coordinate with us before you spend too much time working on an implementation! -->
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: 🤗 Support Question
|
||||
about: If you have a question 💬, or something was not clear from the docs!
|
||||
|
||||
---
|
||||
|
||||
<!-- ^ Click "Preview" for a nicer view! ^
|
||||
We primarily use GitHub as an issue tracker. If however you're encountering an issue not covered in the docs, we may be able to help! -->
|
||||
|
||||
---
|
||||
|
||||
Please make sure you have read our [main Readme](https://github.com/numerique-gouv/meet).
|
||||
|
||||
Also make sure it was not already answered in [an open or close issue](https://github.com/numerique-gouv/meet/issues).
|
||||
|
||||
If your question was not covered, and you feel like it should be, fire away! We'd love to improve our docs! 👌
|
||||
|
||||
**Topic**
|
||||
What's the general area of your question: for example, docker setup, database schema, search functionality,...
|
||||
|
||||
**Question**
|
||||
Try to be as specific as possible so we can help you as best we can. Please be patient 🙏
|
||||
@@ -0,0 +1,11 @@
|
||||
## Purpose
|
||||
|
||||
Description...
|
||||
|
||||
|
||||
## Proposal
|
||||
|
||||
Description...
|
||||
|
||||
- [] item 1...
|
||||
- [] item 2...
|
||||
@@ -13,10 +13,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Download Crowdin files
|
||||
uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0
|
||||
uses: crowdin/github-action@v2
|
||||
with:
|
||||
upload_sources: false
|
||||
upload_translations: false
|
||||
|
||||
@@ -19,8 +19,6 @@ env:
|
||||
DOCKER_USER: 1001:127
|
||||
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
|
||||
DOCKER_CONTAINER_REGISTRY_NAMESPACE: lasuite
|
||||
IS_MULTI_PLATFORM_BUILD: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
BUILD_PLATFORMS: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
|
||||
|
||||
jobs:
|
||||
build-and-push-backend:
|
||||
@@ -30,42 +28,41 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '--target backend-production -f Dockerfile'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
target: backend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -76,43 +73,42 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f src/frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./src/frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -123,43 +119,42 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
with:
|
||||
docker-build-args: '-f docker/dinum-frontend/Dockerfile --target frontend-production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: .
|
||||
file: ./docker/dinum-frontend/Dockerfile
|
||||
target: frontend-production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -170,30 +165,29 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
@@ -201,14 +195,14 @@ jobs:
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./src/summary
|
||||
file: ./src/summary/Dockerfile
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -219,30 +213,29 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: env.IS_MULTI_PLATFORM_BUILD == 'true'
|
||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: lasuite/meet-agents
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/')
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@d6e94cfb488f03a0b3e8b8739aad94e74d24d8da # main
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
@@ -250,14 +243,14 @@ jobs:
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
context: ./src/agents
|
||||
file: ./src/agents/Dockerfile
|
||||
target: production
|
||||
platforms: ${{ env.BUILD_PLATFORMS }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
build-args: DOCKER_USER=${{ env.DOCKER_USER }}:-1000
|
||||
push: ${{ github.event_name != 'pull_request' || startsWith(github.head_ref, 'integration/') }}
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
@@ -273,7 +266,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name != 'pull_request'
|
||||
steps:
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@cac2ee67896eb13e84e804f60c4271370424eaa8 # main
|
||||
- uses: numerique-gouv/action-argocd-webhook-notification@main
|
||||
id: notify
|
||||
with:
|
||||
deployment_repo_path: "${{ secrets.DEPLOYMENT_REPO_URL }}"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: meet Workflow
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
@@ -26,17 +26,17 @@ jobs:
|
||||
- name: Enforce absence of print statements in code
|
||||
if: always()
|
||||
run: |
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude).github/workflows/**' | grep "print("
|
||||
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/meet.yml' | grep "print("
|
||||
- name: Check absence of fixup commits
|
||||
if: always()
|
||||
run: |
|
||||
! git log | grep 'fixup!'
|
||||
- name: Install uv
|
||||
- name: Install gitlint
|
||||
if: always()
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
run: pip install --user requests gitlint
|
||||
- name: Lint commit messages added to main
|
||||
if: always()
|
||||
run: uvx --no-build --from gitlint-core==0.19.1 gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
@@ -77,15 +77,15 @@ jobs:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "22"
|
||||
node-version: "18"
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache@v5
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
@@ -93,11 +93,11 @@ jobs:
|
||||
|
||||
- name: Install yarn
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: npm install -g --ignore-scripts yarn@1.22.22
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Install node dependencies
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
run: yarn install --frozen-lockfile --ignore-scripts
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build mails
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
@@ -105,7 +105,7 @@ jobs:
|
||||
|
||||
- name: Cache mail templates
|
||||
if: steps.mail-templates.outputs.cache-hit != 'true'
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache@v5
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
@@ -119,22 +119,22 @@ jobs:
|
||||
working-directory: src/backend
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
run: uv run ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
run: uv run ruff check .
|
||||
- name: Lint code with pylint
|
||||
run: uv run --no-sync --no-build pylint meet demo core
|
||||
run: uv run pylint meet demo core
|
||||
|
||||
lint-agents:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -145,19 +145,18 @@ jobs:
|
||||
working-directory: src/agents
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras --no-build
|
||||
cache: "pip"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
run: ~/.local/bin/ruff check .
|
||||
|
||||
lint-summary:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -168,19 +167,18 @@ jobs:
|
||||
working-directory: src/summary
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
cache: "pip"
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
- name: Check code formatting with ruff
|
||||
run: uv run --no-sync --no-build ruff format . --diff
|
||||
run: ~/.local/bin/ruff format . --diff
|
||||
- name: Lint code with ruff
|
||||
run: uv run --no-sync --no-build ruff check .
|
||||
run: ~/.local/bin/ruff check .
|
||||
|
||||
test-back:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -225,10 +223,11 @@ jobs:
|
||||
DB_PORT: 5432
|
||||
REDIS_URL: redis://localhost:6379/1
|
||||
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
LIVEKIT_API_SECRET: secret
|
||||
LIVEKIT_API_KEY: devkey
|
||||
AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
AWS_S3_ACCESS_KEY_ID: meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY: meet-secret-access-key
|
||||
AWS_S3_REGION_NAME: local
|
||||
AWS_S3_ACCESS_KEY_ID: meet
|
||||
AWS_S3_SECRET_ACCESS_KEY: password
|
||||
OIDC_RS_CLIENT_ID: meet
|
||||
OIDC_RS_CLIENT_SECRET: ThisIsAnExampleKeyForDevPurposeOnly
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT: https://oidc.example.com/introspect
|
||||
@@ -237,7 +236,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Create writable /data
|
||||
run: |
|
||||
@@ -245,35 +244,45 @@ jobs:
|
||||
sudo mkdir -p /data/static
|
||||
|
||||
- name: Restore the mail templates
|
||||
uses: actions/cache@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
|
||||
uses: actions/cache@v5
|
||||
id: mail-templates
|
||||
with:
|
||||
path: "src/backend/core/templates/mail"
|
||||
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
|
||||
|
||||
# Creates the access key and the bucket on startup
|
||||
- name: Start Garage
|
||||
- name: Start MinIO
|
||||
run: |
|
||||
docker run -d --name garage \
|
||||
docker pull minio/minio
|
||||
docker run -d --name minio \
|
||||
-p 9000:9000 \
|
||||
-v "${GITHUB_WORKSPACE}/docker/files/etc/garage/garage.toml:/etc/garage.toml:ro" \
|
||||
-e "GARAGE_RPC_SECRET=$(openssl rand -hex 32)" \
|
||||
-e "GARAGE_DEFAULT_ACCESS_KEY=meet-access-key" \
|
||||
-e "GARAGE_DEFAULT_SECRET_KEY=meet-secret-access-key" \
|
||||
-e "GARAGE_DEFAULT_BUCKET=meet-media-storage" \
|
||||
dxflrs/garage:v2.4.1 \
|
||||
/garage server --single-node --default-bucket
|
||||
-e "MINIO_ACCESS_KEY=meet" \
|
||||
-e "MINIO_SECRET_KEY=password" \
|
||||
-v /data/media:/data \
|
||||
minio/minio server --console-address :9001 /data
|
||||
|
||||
- name: Wait for Garage to be ready
|
||||
# Tool to wait for a service to be ready
|
||||
- name: Install Dockerize
|
||||
run: |
|
||||
timeout 30 sh -c 'until docker exec garage /garage health; do sleep 1; done'
|
||||
curl -sSL https://github.com/jwilder/dockerize/releases/download/v0.8.0/dockerize-linux-amd64-v0.8.0.tar.gz | sudo tar -C /usr/local/bin -xzv
|
||||
|
||||
- name: Wait for MinIO to be ready
|
||||
run: |
|
||||
dockerize -wait tcp://localhost:9000 -timeout 10s
|
||||
|
||||
- name: Configure MinIO
|
||||
run: |
|
||||
MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
|
||||
docker exec ${MINIO} sh -c \
|
||||
"mc alias set meet http://localhost:9000 meet password && \
|
||||
mc alias ls && \
|
||||
mc mb meet/meet-media-storage"
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
uses: astral-sh/setup-uv@v7
|
||||
- name: Install the dependencies
|
||||
run: uv sync --locked --all-extras
|
||||
|
||||
@@ -283,10 +292,10 @@ jobs:
|
||||
sudo apt-get install -y gettext
|
||||
|
||||
- name: Generate a MO file from strings extracted from the project
|
||||
run: uv run --no-sync --no-build python manage.py compilemessages
|
||||
run: uv run python manage.py compilemessages
|
||||
|
||||
- name: Run tests
|
||||
run: uv run --no-sync --no-build pytest -n 2
|
||||
run: uv run pytest -n 2
|
||||
|
||||
test-summary:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -297,11 +306,11 @@ jobs:
|
||||
working-directory: src/summary
|
||||
|
||||
env:
|
||||
AUTHORIZED_TENANTS: '[{"id": "test-tenant", "api_key": "test-api-token", "webhook_url": "https://example.com/webhook", "webhook_api_key": "test-webhook-api-key"}]'
|
||||
APP_API_TOKEN: "test-api-token"
|
||||
AWS_STORAGE_BUCKET_NAME: "http://meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL: "garage:9000"
|
||||
AWS_S3_ACCESS_KEY_ID: "meet-access-key"
|
||||
AWS_S3_SECRET_ACCESS_KEY: "meet-secret-access-key"
|
||||
AWS_S3_ENDPOINT_URL: "minio:9000"
|
||||
AWS_S3_ACCESS_KEY_ID: "meet"
|
||||
AWS_S3_SECRET_ACCESS_KEY: "password"
|
||||
WHISPERX_BASE_URL: "https://configure-your-url.com"
|
||||
WHISPERX_ASR_MODEL: "large-v2"
|
||||
WHISPERX_API_KEY: "test-whisperx-secret"
|
||||
@@ -309,29 +318,24 @@ jobs:
|
||||
LLM_BASE_URL: "https://configure-your-url.com"
|
||||
LLM_API_KEY: "test-llm-secret"
|
||||
LLM_MODEL: "test-llm-model"
|
||||
WEBHOOK_API_TOKEN: "test-webhook-secret"
|
||||
WEBHOOK_URL: "https://configure-your-url.com"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
|
||||
- name: Install ffmpeg
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y ffmpeg
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.13"
|
||||
cache: "pip"
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
||||
|
||||
- name: Install the project
|
||||
run: uv sync --locked --all-extras
|
||||
- name: Install development dependencies
|
||||
run: pip install --user .[dev]
|
||||
|
||||
- name: Run summary tests
|
||||
run: uv run --no-sync --no-build pytest
|
||||
run: ~/.local/bin/pytest
|
||||
|
||||
lint-front:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -339,10 +343,10 @@ jobs:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: cd src/frontend/ && npm ci --ignore-scripts
|
||||
run: cd src/frontend/ && npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: cd src/frontend/ && npm run lint
|
||||
@@ -359,10 +363,10 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm ci
|
||||
|
||||
- name: Check linting
|
||||
run: npm run lint
|
||||
@@ -380,10 +384,10 @@ jobs:
|
||||
working-directory: src/sdk/library
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci --ignore-scripts
|
||||
run: npm ci
|
||||
|
||||
- name: Build SDK
|
||||
run: npm run build
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@@ -21,12 +21,12 @@ jobs:
|
||||
run: rm -rf ./src/helm/extra
|
||||
|
||||
- name: Install Helm
|
||||
uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1
|
||||
uses: azure/setup-helm@v4
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Publish Helm charts
|
||||
uses: numerique-gouv/helm-gh-pages@2cf477ae49d7c70037ceb1685803f4f7bad9b981 # add-overwrite-option
|
||||
uses: numerique-gouv/helm-gh-pages@add-overwrite-option
|
||||
with:
|
||||
charts_dir: ./src/helm
|
||||
linting: on
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
# /!\
|
||||
# Security Note: This action is not hardened against prompt injection attacks and should only be used
|
||||
# to review trusted PRs. Configure your repository with "Require approval for all external contributors"
|
||||
# to ensure workflows only run after a maintainer has reviewed the PR.
|
||||
name: Security Review
|
||||
|
||||
permissions:
|
||||
pull-requests: write # Needed for leaving PR comments
|
||||
contents: read
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
fetch-depth: 2
|
||||
|
||||
- uses: anthropics/claude-code-security-review@0c6a49f1fa56a1d472575da86a94dbc1edb78eda
|
||||
with:
|
||||
comment-pr: true
|
||||
exclude-directories: docs,gitlint,LICENSES,bin
|
||||
claude-api-key: ${{ secrets.CLAUDE_API_KEY }}
|
||||
@@ -83,9 +83,3 @@ docker/livekit/out
|
||||
|
||||
# LiveKit CA configuration
|
||||
docker/livekit/rootCA.pem
|
||||
|
||||
# Frontend rollup-plugin-visualizer
|
||||
/src/frontend/rollup-plugin-visualizer/*
|
||||
|
||||
# NixOS
|
||||
.devenv
|
||||
|
||||
@@ -10,544 +10,18 @@ and this project adheres to
|
||||
|
||||
### Added
|
||||
|
||||
- 🔒(backend) throttle meeting link generation
|
||||
- 🔒️(backend) add a daily cap on room creation
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(backend) update python dependencies
|
||||
- ⬆️(summary) update python dependencies
|
||||
- ⬆️(agents) update python dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- ⚡️(frontend) disable posthog-js periodic feature flag reloads
|
||||
|
||||
## [1.32.1] - 2026-09-25
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔒️(backend) fix CVE-2026-73228 and CVE-2026-73229 in drf
|
||||
- 🔒️(agent) fix CRITICAL CVE-2026-63072 / CVE-2026-63073 in libssl3t64
|
||||
|
||||
## [1.32.0] - 2026-09-25
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) make the LiveKit default video codec configurable
|
||||
- ✨(backend) purge rooms inactive for a configurable period
|
||||
- 🔧(dev) add support for Bureautix workstations
|
||||
- ✨(frontend) add screen share zoom controls #1498
|
||||
- 🔨(makefile) add targets to list and download files stored in Garage
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔥(backend) remove unused API viewset and permission helpers
|
||||
- 🔊(backend) pin the dockerflow logger level to WARNING
|
||||
- 🚑️(summary) serve health endpoints with the dockerflow router
|
||||
- ♻️(backend) serve the dockerflow views early in the middleware stack
|
||||
- 📈(frontend) include LiveKit SIDs in the connection analytics event
|
||||
- 🔇(backend) silence expected 401 warnings on /me
|
||||
- 🔇(backend) silence noisy request summary info logs
|
||||
- ⚡️(frontend) defer loading the Crisp script until idle
|
||||
- ⬆️(frontend) upgrade react-i18next from 17.0.10 to 17.0.12
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.414.0 to 1.418.10
|
||||
- ⬆️(addons) upgrade i18next from 26.3.6 to 26.4.0
|
||||
- ⬆️(frontend) upgrade humanize-duration from 3.33.2 to 3.34.1
|
||||
- ⬆️(addons) upgrade i18next from 26.4.0 to 26.4.2
|
||||
- 🔖(helm) release chart 0.0.28
|
||||
- ♻️(backend) decouple recording event handling from LiveKit egress statuses
|
||||
- ♻️(agents) replace the minio client by boto3
|
||||
- 🔧(compose) replace MinIO by Garage for local development
|
||||
- 🔧(helm) point media services to Garage by default
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(helm) probe liveness on __lbheartbeat__ and readiness on __heartbeat__
|
||||
- 🐛(helm) render periodSeconds and failureThreshold on probes
|
||||
- 🐛(backend) report the app release to Sentry instead of "NA"
|
||||
- 🐛(frontend) play the waiting room notification sound on every arrival
|
||||
- 🐛(frontend) apply saved reception resolution when joining a meeting #1714
|
||||
- 🐛(backend) acknowledge unknown LiveKit webhook events instead of 422
|
||||
- 🔒️(backend) enforce display name setting on rename API
|
||||
- 🔒️(backend) reject inactive users in resource server backend
|
||||
- 🐛(frontend) fix file permissions in the Docker image
|
||||
- 🚸(frontend) inform user that recording waits until a track is published
|
||||
- 🔒(backend) upgrade base image to python:3.13.5-alpine3.24
|
||||
- 🐛(backend) handle failed and aborted egresses
|
||||
- 🩹(frontend) notify participants when a recording fails or is aborted
|
||||
- 🔒️(frontend) fix HIGH CVE-2026-93990 in libexpat
|
||||
|
||||
## [1.31.0] - 2026-09-08
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add 1080p sending resolution option #1660
|
||||
- ✨(backend) add Traefik support via configurable media-auth url header #1649
|
||||
- ✨(backend) update a room's attributes from the external API
|
||||
- 🔊(backend) log request duration in Gunicorn workers
|
||||
- 📈(frontend) track missing lobby participant on accept/reject
|
||||
- ✨(backend) sort waiting participants by their arrival time
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(dev) pin LiveKit server to v1.13.6
|
||||
- 🔒(frontend) upgrade base image to 1.30.4-alpine3.24
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) allow any printable ASCII characters in user sub field #1673
|
||||
- 🐛(frontend) keep the sending resolution picked while the camera is off #1667
|
||||
- 🐛(frontend) restore automatic lower-hand on speaking
|
||||
- 🐛(frontend) center Avatar initials with a font-aware cap-height ratio
|
||||
- 🐛(frontend) keep feedback buttons on one line for fr/es/en
|
||||
- ⚡️(frontend) increase lobby polling interval on both sides
|
||||
- ⚡️(frontend) add trailing slash on the /me endpoint call
|
||||
- ⚡️(backend) refactor lobby storage to bound key lookups per room
|
||||
- ⚡️(backend) refactor presence cache to bound key lookups per room
|
||||
- 💄(frontend) position the login hint dynamically next to the button
|
||||
|
||||
## [1.30.0] - 2026-09-01
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(agent) support Voxtral realtime as inference engine
|
||||
- 🌐(i18n) add Spanish language support
|
||||
- ✨(frontend) expose publish permissions on the media state element #1661
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔥(backend) remove the S3 storage-event webhook for recordings
|
||||
- ♻️(backend) always finalize recordings using the LiveKit egress_ended webhook
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.409.5 to 1.414.0
|
||||
- ⬆️(frontend) upgrade @fontsource-variable/lexend from 5.2.11 to 5.3.0
|
||||
- ⬆️(frontend) upgrade @fontsource/opendyslexic from 5.2.5 to 5.3.0
|
||||
- ⬆️(addons) upgrade core-js from 3.49.0 to 3.50.0
|
||||
- ♻️(backend) factorize s3 client creation in utils
|
||||
- ♿️(frontend) close side panel with Escape key #1507
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) fix chat text-area bug
|
||||
|
||||
## [1.29.0] - 2026-08-25
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(any) let any authenticated user manage the lobby on trusted rooms
|
||||
|
||||
### Changed
|
||||
|
||||
- 📱(frontend) collapse mobile control bar items on narrow viewports
|
||||
- 📱(frontend) stack idle modal buttons in a column on mobile
|
||||
- 📱(frontend) improve feedback screen responsiveness on mobile
|
||||
- ⬆️(frontend) upgrade @fontsource-variable/atkinson-hyperlegible-next
|
||||
- ⬆️(frontend) upgrade i18next-resources-to-backend from 1.2.1 to 1.2.3
|
||||
- ⬆️(frontend) upgrade @tanstack/react-query from 5.101.1 to 5.101.4
|
||||
- ⬆️(frontend) upgrade @pandacss/preset-panda from 1.11.3 to 1.12.0
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.404.1 to 1.409.5
|
||||
- ⚡️(frontend) apply frugal constraint to the active meeting audio track
|
||||
- ⚡️(backend) replace blocking Redis KEYS with cursor-based SCAN
|
||||
- ✨(summary) add hostname to analytics properties
|
||||
|
||||
## [1.28.0] - 2026-08-24
|
||||
|
||||
### Added
|
||||
|
||||
- 📈(frontend) track errors when starting or stopping a recording
|
||||
- 🚸(frontend) explain camera-in-use failures on the join screen
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(backend) accept form-urlencoded on the user token endpoint
|
||||
- ✨(summary) configurable s3 region
|
||||
- ⬆️(frontend) upgrade i18next and react-i18next patch versions
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.395.0 to 1.404.1
|
||||
- ⬆️(frontend) upgrade livekit-client and @livekit/components-react
|
||||
- 💄(frontend) increase the blur intensity
|
||||
|
||||
### Fixed
|
||||
|
||||
- 📝(docs) fix minor typos in comments and docstrings
|
||||
- ⬆️(backend) bump sqlparse from 0.5.5 to 0.6.0
|
||||
- ⬆️(mail) bump @html-to/text-cli from 0.6.0 to 0.6.1
|
||||
- 🐛(frontend) treat client-initiated connect aborts as events
|
||||
- 🐛(frontend) use state instead of a ref for MoreControls container
|
||||
- 🐛(frontend) stop init_virtual_background from firing on blur updates
|
||||
- 🐛(frontend) hoist mute confirmation dialog to VideoConference level
|
||||
- 🐛(frontend) fix joined notification tile no longer rendering properly
|
||||
- 🐛(frontend) handle device-in-use errors on Chrome / Windows 10
|
||||
- 🐛(frontend) handle Firefox/Windows AbortError on device start
|
||||
- 🐛(frontend) treat "Timeout starting source" AbortError as device-in-use
|
||||
- 🔇(frontend) suppress leaked WebSocket error events from livekit-client
|
||||
|
||||
## [1.27.0] - 2026-08-14
|
||||
|
||||
### Changed
|
||||
|
||||
- 🔥(frontend) drop unused vendored ConnectionObserver
|
||||
- 🐛(frontend) vendor formatChatMessageLinks and trim surrounding newlines
|
||||
|
||||
### Fixed
|
||||
|
||||
- 📈(frontend) downgrade unreachable external home URL from error to event
|
||||
- 🐛(frontend) handle 401 responses when syncing user preferences
|
||||
- 🐛(frontend) harden speaker test against missing sinks and play errors
|
||||
- 🐛(frontend) implement hysteresis band for the control bar layout
|
||||
- 🐛(frontend) fix toolbar ResizeObserver loop and alignment drift
|
||||
- 🐛(analytics) filter benign ResizeObserver loop error in Sentry/PostHog
|
||||
- 🐛(frontend) stop reporting screen-share denials as errors
|
||||
- 🐛(frontend) generalize screen-share error modal beyond macOS
|
||||
- 📈(frontend) stop double-reporting media device failures
|
||||
|
||||
## [1.26.0] - 2026-08-12
|
||||
|
||||
### Added
|
||||
|
||||
- 📈(frontend) capture media diagnostics on media errors
|
||||
- ✨(frontend) add an audio gauge to the microphone select menu
|
||||
- ✨(frontend) add a sound tester to the output select menu
|
||||
- ✨(frontend) prompt for permissions when toggling a denied device
|
||||
- ⚗️(frontend) capture console.error in PostHog
|
||||
- 📈(frontend) snapshot media devices on the happy path
|
||||
- 🚸(frontend) guide users when the OS blocks browser media access
|
||||
- ✨(frontend) add a silent-microphone watcher on join and room screens
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(frontend) encapsulate error tracking behind a telemetry module
|
||||
- ♻️(frontend) encapsulate PostHog capture calls in the telemetry module
|
||||
- 🔧(frontend) sync persisted device ids with the actual selected devices
|
||||
- 💄(frontend) hide the ProConnect button on narrow viewports
|
||||
- ♻️(frontend) prefer captureMediaEvent over reportError when no-op
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) drop exact deviceId constraint on dynamic track creation
|
||||
- 🐛(frontend) fix permission store regression
|
||||
- 🐛(frontend) handle missing device errors gracefully
|
||||
- 🐛(frontend) display the meeting id in the join screen page title
|
||||
|
||||
## [1.25.2] - 2026-08-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) serve MediaPipe assets under a versioned path
|
||||
- 🐛(frontend) harmonize cache configuration for MediaPipe assets
|
||||
|
||||
## [1.25.1] - 2026-08-06
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🚑️(frontend) fix background crash from MediaPipe WASM version mismatch
|
||||
|
||||
## [1.25.0] - 2026-08-05
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(summary) report exception type in failure analytics
|
||||
- ✨(frontend) add configurable documentation menu item
|
||||
- ✨(frontend) allow promoting authenticated participants
|
||||
- ✨(frontend) introduce an "unauthenticated" participant badge
|
||||
- ✨(backend) add roomkit viewset to start a room without WebRTC join
|
||||
- ✨(frontend) let users set default configuration for generated links
|
||||
- ✨(frontend) expose media state to external gateways
|
||||
- ✨(frontend) add connection test feature
|
||||
- ✨(sdk) allow passing a background color to the calendar iframe
|
||||
- ✨(sdk) add a room configuration popup from CreateMeetingButton
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(frontend) upgrade @mediapipe/tasks-vision from 0.10.14 to 0.10.35
|
||||
- ⬆️(frontend) upgrade i18next from 26.3.1 to 26.3.6
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.391.2 to 1.395.0
|
||||
- ⬆️(frontend) upgrade @tanstack/react-query from 5.101.0 to 5.101.1
|
||||
- ⬆️(frontend) upgrade livekit-client from 2.19.2 to 2.20.0
|
||||
- ⚡️(frontend) limit unnecessary re-renders #1510
|
||||
- 📝(legal) update terms of service
|
||||
- 💄(frontend) render Avatar initials in uppercase
|
||||
- 💄(frontend) improve participant name rendering in the list
|
||||
- 🚚(backend) rename TelephonyService to SIPManagement
|
||||
- ⬆️(dependencies) update python dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(transcription) fix silent bug in speaker assignment
|
||||
- 🐛(summary) extend tasks auto retry logic
|
||||
- 🐛(summary) properly detect when failure webhook should be sent
|
||||
- 🐛(backend) preserve recording metadata when updating room access
|
||||
- 🐛(backend) allow any string as sub in the API serializer
|
||||
- 🐛(frontend) fall back to user.full_name on request-entry
|
||||
- 🚸(frontend) show two initials in the Avatar when possible
|
||||
- 🩹(all) clear the SonarCloud reliability finding and the lint debt
|
||||
- 🐛(frontend) stop the installed app reopening the room it came from
|
||||
- 🐛(backend) serialize lazy title in summary payload
|
||||
- 💄(frontend) show pointer cursor on interactive switches
|
||||
- 🐛(frontend) fix icon centering in the Switch primitive
|
||||
- 🐛(frontend) keep Unicode initials intact in avatar
|
||||
- 🐛(frontend) prevent concurrent settings updates from overwriting each other
|
||||
|
||||
## [1.24.0] - 2026-07-21
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) allow searching the recording admin table by owner email
|
||||
- ✨(frontend) add participant color gradient when camera is off #1490
|
||||
- ✨(all) allow forcing SSO display name for authenticated users
|
||||
- ➕(frontend) install vite-plugin-static-copy for MediaPipe WASM assets
|
||||
- ✨(addon) show add-in tools when creating meetings in shared calendars
|
||||
|
||||
### Changed
|
||||
|
||||
- 🗑️(settings) deprecate SUMMARY_SERVICE_VERSION=1
|
||||
- ⬆️(mail) update mjml to v5 and @html-to/text-cli
|
||||
- 🚸(frontend) initialize the join input name with the persisted full name
|
||||
- ♻️(frontend) refactor background processors to use the new API
|
||||
- ♻️(frontend) inline model weights to avoid loading them from remote
|
||||
- ♻️(frontend) inline MediaPipe WASM modules to avoid loading from remote
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.387.0 to 1.391.2
|
||||
- ⬆️(frontend) upgrade react-stately from 3.47.0 to 3.48.0
|
||||
- ⬆️(frontend) upgrade react-aria from 3.49.0 to 3.50.0
|
||||
- ⬆️(frontend) upgrade react-aria-components from 1.18.0 to 1.19.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🩹(backend) identify externally provisioned users to PostHog
|
||||
- 🐛(backend) fix info panel crash for unregistered rooms
|
||||
- ♿️(frontend) focus side panel container on open #1452
|
||||
- 🐛(summary) whisper call error handling
|
||||
|
||||
## [1.23.0] - 2026-07-08
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) extend analytics module to support feature flags
|
||||
- ✨(backend) implement feature flags in Posthog analytics backend
|
||||
- ✨(agents) report errors to Sentry for all LiveKit agents
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(agents) upgrade to python 3.14 slim
|
||||
- ⬆️(dependencies) update python dependencies
|
||||
- 💥(summary) remove v1 related code #1362
|
||||
- ✨(meet) use compatible with summary v2 #1362
|
||||
- ♻️(backend) refactor analytics backend from Protocol to abstract class
|
||||
- 🔥(summary) remove call to summary enabled feature flag
|
||||
- ♻️(frontend) wrap MuteEveryoneButton with AdminOrOwnerOnly
|
||||
- ⬆️(frontend) upgrade livekit-client from 2.19.0 to 2.19.2
|
||||
- ⬆️(frontend) upgrade posthog-js from 1.386.5 to 1.387.0
|
||||
- ⬆️(frontend) upgrade @tanstack/react-query from 5.100.14 to 5.101.0
|
||||
- ⬆️(frontend) update the frontend build image to Node 22
|
||||
- 🔒️(frontend) update docker image to nginx-unprivileged:1.30.3-alpine3.23
|
||||
- ✨(summary) more precise analytics events
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🚀(front) fix frontend build failure
|
||||
- 🐛(makefile) fix args in make test
|
||||
- 🩹(backend) fix case-insensitive email deduplication in merge command
|
||||
- 🐛(summary) support media files with bad streams #1478
|
||||
|
||||
## [1.22.0] - 2026-07-03
|
||||
|
||||
### 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
|
||||
- 🐛(backend) support unencoded S3 notification object keys #1455
|
||||
- ✨(frontend) prioritize screen share in picture-in-picture layout #1467
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(summary) generalized stt api call #1420
|
||||
- ♻️(env) refactor env variables handling
|
||||
- 🚸(frontend) use "Advanced" instead of "Premium" in the sidepanel
|
||||
- ♿️(frontend) make fullscreen share warning keyboard accessible #1459
|
||||
- ⬆️(summary) update docker alpine to 3.24 & ffmpeg to 8.1.2 #1471
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🛂(backend) reject user access tokens on the API
|
||||
- 🩹(helm) fix Helm ingress rendering when passing multiple hosts
|
||||
|
||||
## [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
|
||||
|
||||
- ✨(backend) add file specific admin #1387
|
||||
|
||||
### Changed
|
||||
|
||||
- 🐛(agents) fix bug when closing metadata-collector
|
||||
- ⬆️(dependencies) update python dependencies
|
||||
- ⬆️(frontend) update js dependencies
|
||||
- ♻️(agents) replace deprecated room options API
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔇(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
|
||||
|
||||
## [1.18.0] - 2026-06-03
|
||||
|
||||
### Added
|
||||
|
||||
- 🔧(backend) backport logging configuration from docs
|
||||
- 🧑💻(backend) add management command to merge duplicate users
|
||||
- 👷(helm) add Kubernetes job for duplicate user merge command
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(backend) prevent duplicate pending users on concurrent requests
|
||||
- 🔒️(backend) prevent file change post checks #1377
|
||||
|
||||
## [1.17.0] - 2026-05-31
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(fullstack) allow participants to mute others based on room configuration
|
||||
- ✨(frontend) add synchronizer for room metadata updates
|
||||
- ✨(frontend) make reaction toolbar responsive on small viewports
|
||||
- ✨(frontend) enable reactions on mobile devices
|
||||
- ✨(frontend) introduce picture-in-picture meeting
|
||||
- ✨(backend) add core.recording.event.parsers.S3Parser
|
||||
- ✨(summary) extended support for all video / audio files #1358
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(fullstack) simplify source serialization
|
||||
- ✨(backend) expose room configuration to all API consumers
|
||||
- 🩹(frontend) improve reaction toolbar centering with dynamic positioning
|
||||
- 🚀 (paas) remove buildpack requirements.txt to use the new uv.lock #1349
|
||||
- ✨(backend) allow room configuration and access level via external api #1260
|
||||
- ♻️(backend) prefix Swagger routes with /api
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🩹(backend) fix swagger and redoc documentation URLs
|
||||
|
||||
## [1.16.0] - 2026-05-13
|
||||
|
||||
### Added
|
||||
|
||||
- 🔒️(backend) add validation of Room.configuration
|
||||
- ✨(helm) add support multiple transcribe worker / endpoint #1247
|
||||
- ✨(backend) make LiveKit Egress recording encoding configurable #1288
|
||||
- ✨(summary) add speaker-to-participant assignment
|
||||
|
||||
### Changed
|
||||
|
||||
- ♻️(summary) change tasks endpoint signature
|
||||
- ⬆️(dependencies) update urllib3 to v2.7.0 [SECURITY]
|
||||
- 🧑💻(agents) use `uv` for package management
|
||||
- ✨(summary) improve speaker-to-participant assignment
|
||||
|
||||
### Fixed
|
||||
|
||||
- ♻(frontend) standardize role terminology across localizations
|
||||
- 🐛(backend) make start-recording atomic and fault-tolerant
|
||||
- 🔒️(frontend) room ids are generated with non-cryptographic rand
|
||||
- ⬆️(mail) fix dependencies not having resolved or integrity field #1321
|
||||
- 🐛(summary) complete webm support #1328
|
||||
- 🐛(backend) add link to "Open" text in recording email
|
||||
- 🩹(frontend) fix spacing regression in mobile control bar
|
||||
|
||||
## [1.15.0] - 2026-04-30
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) add metadata collection of VAD, connection and chat events
|
||||
- ✨(backend) introduce add-ons authentication backend
|
||||
- 💬(backend) clarify french transcription audio download link text #1299
|
||||
- 🚧(addons) introduce initial Microsoft Outlook add-in support (alpha)
|
||||
- 🔧(backend) add setting to toggle application token exchange mechanism
|
||||
- ✨(backend) support add-ons authentication in external viewset
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(summary) support webm #1290
|
||||
- ⬆️(backend) bump django-lasuite to v0.0.26
|
||||
- 🩹(frontend) use a more standard (quality) rating scale
|
||||
- 🩹(frontend) fix access control for screen recording feature flag
|
||||
- 🩹(frontend) fix reconnect loop caused by connectionObserverStore updates
|
||||
|
||||
## [1.14.0] - 2026-04-16
|
||||
|
||||
### Added
|
||||
|
||||
- 🔒️(helm) Add pod and container securityContext #1197
|
||||
- ✨(summary) add routes v2 for async STT and summary tasks #1171
|
||||
- ✅(backend) add unit tests for JwtTokenService #1232
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(backend) bump lodash from 4.17.23 to 4.18.1 in /src/mail
|
||||
- ⬆️(frontend) bump hono from 4.12.8 to 4.12.12 in /src/frontend
|
||||
- ⬆️(backend) bump pygments from 2.19.2 to 2.20.0 in /src/backend
|
||||
- ♻️(backend) use Authorization header for LiveKit token authentication
|
||||
- 🥅(backend) refine Twirp error handling for participant operations
|
||||
- ✨(summary) allow more file extensions #1265
|
||||
- ♿️(frontend) refocus reactions toolbar with ctrl+shift+e is activated #1262
|
||||
- ♿️(frontend) set an explicit document title on recording download page #1261
|
||||
- ♿️(frontend) add customizable accessibility fonts #1270
|
||||
|
||||
### Fixed
|
||||
|
||||
- ⬆️(dependencies) update aiohttp to v3.13.4 [SECURITY]
|
||||
- ⬆️(dependencies) update vite to v7.3.2 [SECURITY]
|
||||
- ⬆️(dependencies) update django to v5.2.13 [SECURITY]
|
||||
- 🔒(backend) rely on backend to allow participant update their metadata
|
||||
- 🐛(summary) fix failure webhook notification #1233
|
||||
- 🐛(summary) relax whisperX payload format #1233
|
||||
- ⬆️(backend) upgrade dependencies to fix Pillow CVE-2026-40192
|
||||
- ⬆️(frontend) upgrade frontend image to Alpine 3.23 to address CVEs
|
||||
|
||||
## [1.13.0] - 2026-03-31
|
||||
- ✨(encryption) opt-in end-to-end encryption for meetings, with the
|
||||
passphrase carried in the URL hash and an explicit "Create an
|
||||
encrypted meeting" entry in the home create menu (gated by a
|
||||
per-user Security preference) #1337
|
||||
- ✨(encryption) authenticated participant emails surfaced in the
|
||||
participants list of encrypted rooms; anonymous participants get a
|
||||
red badge with tooltip in the list, waiting room, and the floating
|
||||
join-request notification
|
||||
|
||||
### Changed
|
||||
|
||||
- ⬆️(dependencies) update python dependencies
|
||||
- ♿️(frontend) add explicit region for call controls #1216
|
||||
- ♿️(frontend) improve accessibility of the reaction toolbar #1216
|
||||
- ♿️(frontend) enhance sidepanel navigation accessibility #1216
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Django Meet
|
||||
|
||||
# ---- base image to inherit from ----
|
||||
FROM python:3.13.15-alpine3.24 AS base
|
||||
FROM python:3.13.5-alpine3.21 AS base
|
||||
|
||||
# Upgrade pip to its latest release to speed up dependencies installation
|
||||
RUN python -m pip install --upgrade pip
|
||||
@@ -37,7 +37,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev
|
||||
|
||||
# ---- mails ----
|
||||
FROM node:22 AS mail-builder
|
||||
FROM node:20 AS mail-builder
|
||||
|
||||
COPY ./src/mail /mail/app
|
||||
|
||||
|
||||
@@ -39,52 +39,21 @@ DB_PORT = 5432
|
||||
DOCKER_UID = $(shell id -u)
|
||||
DOCKER_GID = $(shell id -g)
|
||||
DOCKER_USER = $(DOCKER_UID):$(DOCKER_GID)
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev
|
||||
COMPOSE_RUN = $(COMPOSE) run --rm
|
||||
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev
|
||||
COMPOSE_RUN_LINT_BACK = $(COMPOSE_RUN) --no-deps app-dev
|
||||
COMPOSE_RUN_LINT_AGENTS = $(COMPOSE_RUN) --no-deps multi-user-transcriber-dev
|
||||
COMPOSE_RUN_LINT_SUMMARY = $(COMPOSE_RUN) --no-deps app-summary-dev
|
||||
COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
|
||||
WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s
|
||||
COMPOSE = DOCKER_USER=$(DOCKER_USER) docker compose
|
||||
COMPOSE_EXEC = $(COMPOSE) exec
|
||||
COMPOSE_EXEC_APP = $(COMPOSE_EXEC) app-dev
|
||||
COMPOSE_RUN = $(COMPOSE) run --rm
|
||||
COMPOSE_RUN_APP = $(COMPOSE_RUN) app-dev
|
||||
COMPOSE_RUN_CROWDIN = $(COMPOSE_RUN) crowdin crowdin
|
||||
WAIT_DB = @$(COMPOSE_RUN) dockerize -wait tcp://$(DB_HOST):$(DB_PORT) -timeout 60s
|
||||
|
||||
# -- Backend
|
||||
MANAGE = $(COMPOSE_RUN_APP) python manage.py
|
||||
MAIL_NPM = $(COMPOSE_RUN) -w /app/src/mail node npm
|
||||
|
||||
# -- Linters
|
||||
LINT_RUFF_FORMAT = ruff format .
|
||||
LINT_RUFF_CHECK = ruff check . --fix
|
||||
LINT_PYLINT = pylint meet demo core
|
||||
LINT_BACK = echo 'lint:ruff-format started…' && $(LINT_RUFF_FORMAT) \
|
||||
&& echo 'lint:ruff-check started…' && $(LINT_RUFF_CHECK) \
|
||||
&& echo 'lint:pylint started…' && $(LINT_PYLINT)
|
||||
LINT_AGENTS = echo 'lint:ruff-format started…' && $(LINT_RUFF_FORMAT) \
|
||||
&& echo 'lint:ruff-check started…' && $(LINT_RUFF_CHECK)
|
||||
LINT_SUMMARY = echo 'lint:ruff-format started…' && $(LINT_RUFF_FORMAT) \
|
||||
&& echo 'lint:ruff-check started…' && $(LINT_RUFF_CHECK)
|
||||
|
||||
# -- Frontend
|
||||
PATH_FRONT = ./src/frontend
|
||||
|
||||
# -- Storage
|
||||
GARAGE_BUCKET = meet-media-storage
|
||||
STORAGE_FOLDERS = recordings transcripts summaries
|
||||
STORAGE_DIRS = $(addprefix data/,$(STORAGE_FOLDERS))
|
||||
COMPOSE_RUN_AWS = $(COMPOSE_RUN) --user $(DOCKER_USER)
|
||||
AWS_CLI = garage-cors --endpoint-url=http://garage:9000
|
||||
# Extensions listed in each folder (skips the Egress manifests in recordings/)
|
||||
recordings_EXTENSIONS = mp4 ogg
|
||||
transcripts_EXTENSIONS = json
|
||||
summaries_EXTENSIONS = txt
|
||||
# $(1): folder. Lists its objects with a known extension, most recent first
|
||||
storage_list = s3api list-objects-v2 --bucket $(GARAGE_BUCKET) \
|
||||
--prefix $(1)/
|
||||
storage_query = reverse(sort_by(Contents[?$(foreach ext,$($(1)_EXTENSIONS), \
|
||||
ends_with(Key, `".$(ext)"`) ||) `false`] || `[]`, &LastModified))
|
||||
|
||||
# ==============================================================================
|
||||
# RULES
|
||||
|
||||
@@ -93,9 +62,6 @@ default: help
|
||||
data/media:
|
||||
@mkdir -p data/media
|
||||
|
||||
$(STORAGE_DIRS):
|
||||
@mkdir -p $@
|
||||
|
||||
data/static:
|
||||
@mkdir -p data/static
|
||||
|
||||
@@ -104,14 +70,10 @@ data/static:
|
||||
create-env-files: ## Copy the dist env files to env files
|
||||
create-env-files: \
|
||||
env.d/development/common \
|
||||
env.d/development/garage \
|
||||
env.d/development/crowdin \
|
||||
env.d/development/postgresql \
|
||||
env.d/development/kc_postgresql \
|
||||
env.d/development/summary \
|
||||
env.d/development/kube-secret \
|
||||
env.d/development/multi_user_transcriber \
|
||||
env.d/development/metadata_collector
|
||||
env.d/development/summary
|
||||
.PHONY: create-env-files
|
||||
|
||||
bootstrap: ## Prepare Docker images for the project
|
||||
@@ -132,7 +94,6 @@ bootstrap: \
|
||||
build: ## build the project containers
|
||||
@$(MAKE) build-backend
|
||||
@$(MAKE) build-frontend
|
||||
@$(MAKE) build-agents
|
||||
.PHONY: build
|
||||
|
||||
build-backend: ## build the app-dev container
|
||||
@@ -144,10 +105,6 @@ build-frontend: ## build the frontend container
|
||||
@$(COMPOSE) build frontend
|
||||
.PHONY: build-frontend
|
||||
|
||||
build-agents: ## build the multi-user-transcriber agent container
|
||||
@$(COMPOSE) build multi-user-transcriber-dev
|
||||
.PHONY: build-agents
|
||||
|
||||
down: ## stop and remove containers, networks, images, and volumes
|
||||
@$(COMPOSE) down
|
||||
.PHONY: down
|
||||
@@ -159,7 +116,6 @@ logs: ## display app-dev logs (follow mode)
|
||||
run-backend: ## start only the backend application and all needed services
|
||||
@$(COMPOSE) up --force-recreate -d celery-dev --remove-orphans
|
||||
@$(COMPOSE) up --force-recreate -d nginx
|
||||
@$(COMPOSE) up -d livekit
|
||||
@echo "Wait for postgresql to be up..."
|
||||
@$(WAIT_DB)
|
||||
.PHONY: run-backend
|
||||
@@ -169,24 +125,10 @@ run-summary: ## start only the summary application and all needed services
|
||||
@$(COMPOSE) up --force-recreate -d celery-summary-summarize
|
||||
.PHONY: run-summary
|
||||
|
||||
run-agents: ## start the multi-user-transcriber agent
|
||||
@$(MAKE) run-agent-multi-user-transcriber
|
||||
@$(MAKE) run-agent-metadata-collector
|
||||
.PHONY: run-agents
|
||||
|
||||
run-agent-multi-user-transcriber: ## start the LiveKit agents (multi users transcriber)
|
||||
@$(COMPOSE) up --force-recreate -d multi-user-transcriber-dev
|
||||
.PHONY: run-agent-multi-user-transcriber
|
||||
|
||||
run-agent-metadata-collector: ## start the LiveKit agents (metadata collector)
|
||||
@$(COMPOSE) up --force-recreate -d metadata-collector-dev
|
||||
.PHONY: run-agent-metadata-collector
|
||||
|
||||
run:
|
||||
run: ## start the wsgi (production) and development server
|
||||
@$(MAKE) run-backend
|
||||
@$(MAKE) run-summary
|
||||
@$(MAKE) run-agents
|
||||
@$(COMPOSE) up --force-recreate -d frontend
|
||||
.PHONY: run
|
||||
|
||||
@@ -224,58 +166,47 @@ demo: ## flush db then create a demo for load testing purpose
|
||||
@$(MANAGE) create_demo
|
||||
.PHONY: demo
|
||||
|
||||
lint: ## lint all python sources (back-end, agents, summary)
|
||||
@$(MAKE) lint-back
|
||||
@$(MAKE) lint-agents
|
||||
@$(MAKE) lint-summary
|
||||
# Nota bene: Black should come after isort just in case they don't agree...
|
||||
lint: ## lint back-end python sources
|
||||
lint: \
|
||||
lint-ruff-format \
|
||||
lint-ruff-check \
|
||||
lint-pylint
|
||||
.PHONY: lint
|
||||
|
||||
lint-back: ## lint back-end python sources
|
||||
@$(COMPOSE_RUN_LINT_BACK) sh -c "$(LINT_BACK)"
|
||||
.PHONY: lint-back
|
||||
|
||||
lint-agents: ## lint agents python sources
|
||||
@$(COMPOSE_RUN_LINT_AGENTS) sh -c "$(LINT_AGENTS)"
|
||||
.PHONY: lint-agents
|
||||
|
||||
lint-summary: ## lint summary python sources
|
||||
@$(COMPOSE_RUN_LINT_SUMMARY) sh -c "$(LINT_SUMMARY)"
|
||||
.PHONY: lint-summary
|
||||
|
||||
lint-ruff-format: ## format back-end python sources with ruff
|
||||
@echo 'lint:ruff-format started…'
|
||||
@$(COMPOSE_RUN_LINT_BACK) $(LINT_RUFF_FORMAT)
|
||||
@$(COMPOSE_RUN_APP) ruff format .
|
||||
.PHONY: lint-ruff-format
|
||||
|
||||
lint-ruff-check: ## lint back-end python sources with ruff
|
||||
@echo 'lint:ruff-check started…'
|
||||
@$(COMPOSE_RUN_LINT_BACK) $(LINT_RUFF_CHECK)
|
||||
@$(COMPOSE_RUN_APP) ruff check . --fix
|
||||
.PHONY: lint-ruff-check
|
||||
|
||||
lint-pylint: ## lint back-end python sources with pylint only on changed files from main
|
||||
@echo 'lint:pylint started…'
|
||||
@$(COMPOSE_RUN_LINT_BACK) $(LINT_PYLINT)
|
||||
@$(COMPOSE_RUN_APP) pylint meet demo core
|
||||
.PHONY: lint-pylint
|
||||
|
||||
test: ## run project tests; pass extra pytest args via ARGS, e.g. `make test ARGS="-vv"`
|
||||
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
$(MAKE) test-back-parallel ARGS="$${args}" && \
|
||||
$(MAKE) test-summary ARGS="$${args}"
|
||||
test: ## run project tests
|
||||
@$(MAKE) test-back-parallel
|
||||
@$(MAKE) test-summary
|
||||
.PHONY: test
|
||||
|
||||
test-back: ## run back-end tests (pass extra pytest args via ARGS)
|
||||
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest $${args}
|
||||
test-back: ## run back-end tests
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest $${args:-${1}}
|
||||
.PHONY: test-back
|
||||
|
||||
test-back-parallel: ## run all back-end tests in parallel (pass extra pytest args via ARGS)
|
||||
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest -n auto $${args}
|
||||
test-back-parallel: ## run all back-end tests in parallel
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest -n auto $${args:-${1}}
|
||||
.PHONY: test-back-parallel
|
||||
|
||||
test-summary: ## run summary tests (pass extra pytest args via ARGS)
|
||||
@args="$(ARGS) $(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest-summary $${args}
|
||||
test-summary: ## run summary tests
|
||||
@args="$(filter-out $@,$(MAKECMDGOALS))" && \
|
||||
bin/pytest-summary $${args:-${1}}
|
||||
.PHONY: test-summary
|
||||
|
||||
makemigrations: ## run django makemigrations for the Meet project.
|
||||
@@ -312,7 +243,7 @@ shell: ## connect to database shell
|
||||
# -- Database
|
||||
|
||||
dbshell: ## connect to database shell
|
||||
@$(COMPOSE_EXEC_APP) python manage.py dbshell
|
||||
docker compose exec app-dev python manage.py dbshell
|
||||
.PHONY: dbshell
|
||||
|
||||
resetdb: FLUSH_ARGS ?=
|
||||
@@ -334,41 +265,6 @@ env.d/development/kc_postgresql:
|
||||
env.d/development/summary:
|
||||
cp -n env.d/development/summary.dist env.d/development/summary
|
||||
|
||||
env.d/development/kube-secret:
|
||||
cp -n env.d/development/kube-secret.dist env.d/development/kube-secret
|
||||
|
||||
env.d/development/garage:
|
||||
sed "s/^GARAGE_RPC_SECRET=.*/GARAGE_RPC_SECRET=$$(openssl rand -hex 32)/" \
|
||||
env.d/development/garage.dist > env.d/development/garage
|
||||
|
||||
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
|
||||
|
||||
# -- Storage
|
||||
|
||||
recordings-download-latest: ## download the latest recording from Garage into data/recordings
|
||||
transcripts-download-latest: ## download the latest transcript from Garage into data/transcripts
|
||||
summaries-download-latest: ## download the latest summary from Garage into data/summaries
|
||||
$(STORAGE_FOLDERS:%=%-download-latest): %-download-latest: data/%
|
||||
@key=$$($(COMPOSE_RUN_AWS) -T $(AWS_CLI) $(call storage_list,$*) \
|
||||
--query '$(call storage_query,$*)[0].Key' --output text) && \
|
||||
if [ "$$key" = "None" ]; then echo "No $* found"; exit 1; fi && \
|
||||
$(COMPOSE_RUN_AWS) --volume $(CURDIR)/data/$*:/aws/data/$* \
|
||||
$(AWS_CLI) s3 cp "s3://$(GARAGE_BUCKET)/$$key" data/$*/
|
||||
.PHONY: $(STORAGE_FOLDERS:%=%-download-latest)
|
||||
|
||||
recordings-list: ## list recordings stored in Garage, most recent first
|
||||
transcripts-list: ## list transcripts stored in Garage, most recent first
|
||||
summaries-list: ## list summaries stored in Garage, most recent first
|
||||
$(STORAGE_FOLDERS:%=%-list): %-list:
|
||||
@$(COMPOSE_RUN_AWS) $(AWS_CLI) $(call storage_list,$*) \
|
||||
--query '$(call storage_query,$*)[].{Date: LastModified, Key: Key, "Size (bytes)": Size}' \
|
||||
--output table
|
||||
.PHONY: $(STORAGE_FOLDERS:%=%-list)
|
||||
|
||||
# -- Internationalization
|
||||
|
||||
env.d/development/crowdin:
|
||||
@@ -456,9 +352,15 @@ frontend-i18n-generate: \
|
||||
|
||||
# -- K8S
|
||||
build-k8s-cluster: ## build the kubernetes cluster using kind
|
||||
build-k8s-cluster: \
|
||||
env.d/development/kube-secret \
|
||||
./bin/start-kind.sh
|
||||
./bin/start-kind.sh
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
install-external-secrets: ## install the kubernetes secrets from Vaultwarden
|
||||
./bin/install-external-secrets.sh
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
start-tilt: ## start the kubernetes cluster using kind
|
||||
tilt up --namespace=meet -f ./bin/Tiltfile
|
||||
.PHONY: build-k8s-cluster
|
||||
|
||||
start-tilt-keycloak: ## start the kubernetes cluster using kind, without Pro Connect for authentication, use keycloak
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
<img alt="meet logo" src="./docs/assets/banner-meet-fr.png" maxWidth="100%">
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/suitenumerique/meet/stargazers/">
|
||||
<img src="https://img.shields.io/github/stars/suitenumerique/meet" alt="">
|
||||
@@ -13,13 +12,10 @@
|
||||
<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 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">
|
||||
<a href="https://livekit.io/">LiveKit</a> - <a href="https://matrix.to/#/#meet-official:matrix.org">Chat with us</a> - <a href="https://github.com/orgs/suitenumerique/projects/3/views/2">Roadmap</a> - <a href="https://github.com/suitenumerique/meet/blob/main/CHANGELOG.md">Changelog</a> - <a href="https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md">Bug reports</a>
|
||||
<a href="https://livekit.io/">LiveKit</a> - <a href="https://matrix.to/#/#meet-official:matrix.org">Chat with us</a> - <a href="https://github.com/orgs/suitenumerique/projects/3/views/2">Roadmap</a> - <a href="https://github.com/suitenumerique/meet/blob/main/CHANGELOG.md">Changelog</a> - <a href="https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md">Bug reports</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -32,38 +28,59 @@
|
||||
|
||||
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
|
||||
- Non-persistent, secure chat
|
||||
- End-to-end encryption (coming soon)
|
||||
- End-to-end encryption with passphrase-in-link key distribution
|
||||
- Meeting recording
|
||||
- Meeting transcription & Summary (currently in beta)
|
||||
- Telephony integration
|
||||
- Secure participation with robust authentication and access control
|
||||
- Customizable frontend style
|
||||
- LiveKit Advances features including :
|
||||
- speaker detection
|
||||
- simulcast
|
||||
- end-to-end optimizations
|
||||
- speaker detection
|
||||
- simulcast
|
||||
- end-to-end optimizations
|
||||
- selective subscription
|
||||
- SVC codecs (VP9, AV1)
|
||||
|
||||
### End-to-end encryption
|
||||
|
||||
La Suite Meet is fully self-hostable and released under the MIT License, ensuring complete control and flexibility. It's simple to [get started](https://visio.numerique.gouv.fr/) or [request a demo](mailto:visio@numerique.gouv.fr).
|
||||
La Suite Meet supports end-to-end encryption (E2EE) for meetings, so the media server (LiveKit SFU) cannot read audio, video or screen-share content.
|
||||
|
||||
#### How it works
|
||||
|
||||
- Each encrypted meeting carries a 48-character hex passphrase appended to the URL hash (`#…`) — 192 bits of entropy. The server never sees it; sharing the meeting link shares the key.
|
||||
- Frames are encrypted in the browser via LiveKit's Worker + `crypto.subtle` (AES-GCM); only the media payload is encrypted, codec headers stay clear so the SFU can still packetize RTP.
|
||||
- The runtime "is this call encrypted?" decision keys off the URL hash, not the database flag. The DB column (`Room.encryption_mode`) is only used as a sanity reference: if the URL hash and the server's claim disagree, the joining client surfaces an explicit mismatch screen instead of silently joining in clear or in a private encrypted bubble.
|
||||
|
||||
> **Threat model.** "Server doesn't see plaintext" — not "users are safe from a malicious server." A compromised server could still serve modified JavaScript to a participant, who would then leak their passphrase. The E2EE story protects the media path against a passive or compromised SFU, not against a fully compromised origin.
|
||||
|
||||
#### Encryption mode is set at creation, immutable after
|
||||
|
||||
`Room.encryption_mode` is a string enum (`none` / `basic`) chosen when the room is created and never mutated afterwards — changing it would change the link's semantics, since the passphrase lives in the URL hash. There is no mid-call "pause encryption" mechanism: while a meeting is encrypted, **recording and transcription endpoints reject requests with a 400** (`Recording is unavailable in encrypted rooms.` / `Subtitles are unavailable in encrypted rooms.`), the More-tools panel renders those items disabled with an explanatory banner, and the SIP gateway never gets a dispatch rule for encrypted rooms (so dial-in numbers and PINs aren't allocated). Encrypted rooms are also force-locked to `restricted` access level (lobby admission), since basic E2EE only meaningfully protects against passive eavesdropping if the host vets joiners before they receive the in-URL key.
|
||||
|
||||
#### Opt-in by user
|
||||
|
||||
End-to-end encryption is a per-user preference. In **Settings**, under the **Security** section, signed-in users can flip the **End-to-end encryption** toggle — once enabled, a third "Create an encrypted meeting" entry appears in the home-page create-menu (with its own confirmation modal that lists the disabled features and a "Treat this link like a password" connection-details dialog before the meeting starts). Joining is unaffected by the toggle: any participant clicking a meeting link that carries a valid hash joins encrypted, regardless of their own setting. Authenticated joiners of encrypted rooms cannot edit their displayed name — the server enforces the OIDC name on the JWT.
|
||||
|
||||
#### Configuration
|
||||
|
||||
```env
|
||||
ENCRYPTION_ENABLED=true
|
||||
```
|
||||
|
||||
Setting `ENCRYPTION_ENABLED=false` rejects encrypted-room creation at the API level. Existing encrypted rooms stay encrypted (the mode is immutable), but no new ones can be created.
|
||||
|
||||
La Suite Meet is fully self-hostable and released under the MIT License, ensuring complete control and flexibility. It's simple to [get started](https://visio.numerique.gouv.fr/) or [request a demo](mailto:visio@numerique.gouv.fr).
|
||||
|
||||
We’re continuously adding new features to enhance your experience, with the latest updates coming soon!
|
||||
|
||||
### 🚀 Major roll out to all French public servants
|
||||
|
||||
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))
|
||||
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))
|
||||
|
||||
## Table of Contents
|
||||
|
||||
@@ -74,7 +91,6 @@ On the 29th of January 2026, Prime Minister Sébastien Lecornu, announced the fu
|
||||
- [Philosophy](#philosophy)
|
||||
- [Open source](#open-source)
|
||||
|
||||
|
||||
## Get started
|
||||
|
||||
## Docs
|
||||
@@ -93,59 +109,23 @@ We use Kubernetes for our [production instance](https://visio.numerique.gouv.fr/
|
||||
> Some advanced features (ex: recording, transcription) lack detailed documentation. We're working hard to provide comprehensive guides soon.
|
||||
|
||||
#### 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. |
|
||||
| [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. |
|
||||
| [mosa.cloud](https://mosa.cloud/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
|
||||
|
||||
## 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. 🌱
|
||||
|
||||
### 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).
|
||||
- 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)
|
||||
|
||||
## Philosophy
|
||||
|
||||
@@ -155,7 +135,6 @@ Our users come first. We’re committed to making La Suite Meet as accessible an
|
||||
|
||||
Most of the heavy engineering is handled by the incredible LiveKit team, allowing us to focus on delivering a top-tier product. We follow extreme programming practices, favoring pair programming and quick, iterative releases. Challenge our tech and architecture—simplicity is always our top priority.
|
||||
|
||||
|
||||
## Open-source
|
||||
|
||||
Gov 🇫🇷 supports open source! This project is available under [MIT license](https://github.com/suitenumerique/meet/blob/0cc2a7b7b4f4821e2c4d9d790efa739622bb6601/LICENSE).
|
||||
@@ -167,14 +146,13 @@ To learn more, don't hesitate to [reach out](mailto:visio@numerique.gouv.fr).
|
||||
|
||||
Come help us make La Suite Meet even better. We're growing fast and [would love some help](mailto:visio@numerique.gouv.fr).
|
||||
|
||||
|
||||
## Contributors 🧞
|
||||
|
||||
<a href="https://github.com/suitenumerique/meet/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=suitenumerique/meet" />
|
||||
</a>
|
||||
|
||||
## Credits
|
||||
## Credits
|
||||
|
||||
We're using the awesome [LiveKit](https://livekit.io/) implementation. We're also thankful to the teams behind [Django Rest Framework](https://www.django-rest-framework.org/), [Vite.js](https://vite.dev/), and [React Aria](https://github.com/adobe/react-spectrum) — Thanks for your amazing work!
|
||||
This project is tested with BrowserStack.
|
||||
@@ -183,4 +161,3 @@ This project is tested with BrowserStack.
|
||||
|
||||
Code in this repository is published under the MIT license by DINUM (Direction interministériel du numérique).
|
||||
Documentation (in the docs/) directory is released under the [Etalab-2.0 license](https://spdx.org/licenses/etalab-2.0.html).
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Security is very important to us.
|
||||
|
||||
If you have any issue regarding security, please disclose the information responsibly by submitting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at visio@numerique.gouv.fr
|
||||
If you have any issue regarding security, please disclose the information responsibly submiting [this form](https://vdp.numerique.gouv.fr/p/Send-a-report?lang=en) and not by creating an issue on the repository. You can also email us at visio@numerique.gouv.fr
|
||||
|
||||
We appreciate your effort to make Visio more secure.
|
||||
|
||||
|
||||
@@ -15,72 +15,3 @@ the following command inside your docker container:
|
||||
(Note : in your development environment, you can `make migrate`.)
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Purging inactive rooms
|
||||
|
||||
Rooms now keep track of the last time they were started (`last_started_at`), fed by LiveKit's `room_started` webhook. A new `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for `ROOM_INACTIVITY_DELETION_DAYS` days. See [the room purge documentation](docs/features/room-purge.md).
|
||||
|
||||
- The feature is **disabled by default**: nothing is deleted unless you set `ROOM_INACTIVITY_DELETION_DAYS`.
|
||||
- The migration marks every existing room as started at the time of the upgrade, so no existing room can be purged before a full inactivity period has elapsed after upgrading.
|
||||
- Rooms holding a saved recording their users may still access are kept: any saved recording, or, when `RECORDING_EXPIRATION_DAYS` is set, a saved recording created within that window.
|
||||
- Inactivity is measured from LiveKit's `room_started` webhook: if it is not delivered to your backend, rooms in daily use look inactive and get purged.
|
||||
- When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again and can be reused when a meeting is created from that same URL.
|
||||
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
|
||||
|
||||
### Local development: MinIO replaced by Garage
|
||||
|
||||
The development stacks now use [Garage](https://garagehq.deuxfleurs.fr/) instead of MinIO as S3 storage. Garage keeps its own format in `data/media/meta` and `data/media/data` and cannot read what MinIO left there, so local recordings and files will be lost.
|
||||
|
||||
To migrate a local environment:
|
||||
|
||||
1. Stop the stack and remove its containers, including the former `minio` one: `docker compose down --remove-orphans`
|
||||
2. Optionally reclaim the space used by MinIO: `rm -rf data/media && make data/media`
|
||||
3. In your `env.d/development/*` files, replace `minio:9000` by `garage:9000`, the `meet` / `password` credentials by `meet-access-key` / `meet-secret-access-key`, and add `AWS_S3_REGION_NAME=local` (or delete these files and run `make create-env-files`)
|
||||
4. Run `make create-env-files` to generate `env.d/development/garage`, which holds a random RPC secret for Garage.
|
||||
5. Rebuild the images, since the summary and agent images now install boto3 instead of minio
|
||||
|
||||
### Summary service and metadata collector: boto3 replaces the minio client
|
||||
|
||||
The summary service and the metadata collector agent now talk to S3 through boto3 instead of the minio client, with the same settings.
|
||||
Requests are now signed for `AWS_S3_REGION_NAME` as-is. When it is not set, the region is no longer looked up from the bucket: boto3 falls back to `AWS_DEFAULT_REGION`, then to `us-east-1`. If you left `AWS_S3_REGION_NAME` unset, set it to your provider's region before upgrading, or providers that check the signing region will reject the transcripts, summaries and meeting metadata uploads, as well as their signed URLs.
|
||||
|
||||
Also:
|
||||
- Signed URLs to transcripts and summaries are now always path-style (`<endpoint>/<bucket>/<key>`), whereas the minio client used virtual-hosted-style URLs
|
||||
- The metadata collector now accepts `AWS_S3_ENDPOINT_URL` with or without a scheme, like the summary service: the scheme always follows `AWS_S3_SECURE_ACCESS`.
|
||||
|
||||
### Helm chart: media services default to Garage
|
||||
|
||||
The `meet` chart now defaults `serviceMedia.host` and `serviceMediaFiles.host` to `garage.meet.svc.cluster.local`, and the `upstream-vhost` annotation of `ingressMedia` and `ingressMediaFiles` to `garage.meet.svc.cluster.local:9000`. If you relied on the former `minio.meet.svc.cluster.local` defaults, set these values explicitly to your S3 service before upgrading, or recordings and files stop being served under `/media`.
|
||||
|
||||
## v1.30.0
|
||||
|
||||
### Removing S3 storage-event webhooks for recordings
|
||||
|
||||
Recordings were previously confirmed as saved by an S3 storage-event webhook posting to `/api/v1.0/recordings/storage-hook/`. That endpoint has been removed: recordings are now always finalized from LiveKit's own `egress_ended` webhook, which has been the default path since v1.22.0.
|
||||
|
||||
**Required for every deployment:** LiveKit must be able to deliver webhooks to the backend at `/api/v1.0/rooms/webhooks-livekit/`. This is now the only way a recording reaches a saved state; if `egress_ended` is never delivered, recordings stay in the `active` state.
|
||||
|
||||
For hosters who had configured storage-event webhooks:
|
||||
- Recordings reach the same final state, but they are now finalized when LiveKit reports the egress as ended rather than when the storage backend reports the upload.
|
||||
- Remove the event notification from your bucket configuration: it now targets a non-existent endpoint and will fail on every delivery.
|
||||
|
||||
For hosters who had **not** configured storage-event webhooks:
|
||||
- Nothing changes. Recordings have been finalized from the `egress_ended` webhook since v1.22.0.
|
||||
|
||||
In both cases, the following settings are no longer used and can be removed from your env: `RECORDING_EVENT_PARSER_CLASS`, `RECORDING_ENABLE_STORAGE_EVENT_AUTH`, `RECORDING_STORAGE_EVENT_ENABLE`, `RECORDING_STORAGE_EVENT_TOKEN`.
|
||||
|
||||
On completion of the egress, a recording moves to `notification_succeeded`, or to `saved` if notifying external services failed.
|
||||
|
||||
## v1.23.0
|
||||
|
||||
As part of the 1.23.0 release, the legacy `api/v1` implementation has been removed from the _experimental_ Summary service and Meet has been migrated to the new `api/v2`.
|
||||
|
||||
**To avoid a breaking change, the Meet backend continues to use the Summary service's v1-compatible API format by default (`SUMMARY_SERVICE_VERSION` setting defaults to `1`).**
|
||||
|
||||
If you are deploying both Meet and Summary from this repository, you must configure the Meet backend to use the v2 API by setting the following environment variable `SUMMARY_SERVICE_VERSION=2`.
|
||||
|
||||
If you are upgrading only the Meet deployment while keeping an older Summary v1 compatible deployment, no action is required, as the v1-compatible API remains the default.
|
||||
|
||||
Note that we plan on removing the legacy `v1` summary compatibility in a future major version. If you have your own implementation for the summary service, we recommend updating its API contract and setting `SUMMARY_SERVICE_VERSION=2`.
|
||||
|
||||
@@ -2,7 +2,7 @@ load('ext://uibutton', 'cmd_button', 'bool_input', 'location')
|
||||
load('ext://namespace', 'namespace_create', 'namespace_inject')
|
||||
namespace_create('meet')
|
||||
|
||||
DEV_ENV = os.getenv('DEV_ENV', 'dev-keycloak')
|
||||
DEV_ENV = os.getenv('DEV_ENV', 'dev')
|
||||
|
||||
if DEV_ENV == 'dev-dinum':
|
||||
update_settings(suppress_unused_image_warnings=["localhost:5001/meet-frontend-generic:latest"])
|
||||
@@ -23,8 +23,8 @@ docker_build(
|
||||
live_update=[
|
||||
sync('../src/backend', '/app'),
|
||||
run(
|
||||
'uv sync --locked --no-dev',
|
||||
trigger=['../src/backend/uv.lock', '../src/backend/pyproject.toml']
|
||||
'pip install -r /app/requirements.txt',
|
||||
trigger=['./api/requirements.txt']
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -34,11 +34,10 @@ docker_build(
|
||||
'localhost:5001/meet-frontend-dinum:latest',
|
||||
context='..',
|
||||
dockerfile='../docker/dinum-frontend/Dockerfile',
|
||||
only=['./src/frontend', './src/addons', './docker', './.dockerignore'],
|
||||
only=['./src/frontend', './docker', './.dockerignore'],
|
||||
target = 'frontend-production',
|
||||
live_update=[
|
||||
sync('../src/frontend', '/home/frontend'),
|
||||
sync('../src/addons', '/home/addons'),
|
||||
]
|
||||
)
|
||||
clean_old_images('localhost:5001/meet-frontend-dinum')
|
||||
@@ -96,34 +95,18 @@ docker_build(
|
||||
)
|
||||
clean_old_images('localhost:5001/meet-livekit')
|
||||
|
||||
load('ext://secret', 'secret_yaml_generic')
|
||||
k8s_yaml(secret_yaml_generic(
|
||||
name="secret-dev",
|
||||
from_env_file="../env.d/development/kube-secret"
|
||||
))
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev} template .'))
|
||||
|
||||
k8s_yaml(local('cd ../src/helm && helmfile -n meet -e ${DEV_ENV:-dev-keycloak} template .'))
|
||||
|
||||
k8s_resource('garage-cors', resource_deps=['garage'])
|
||||
k8s_resource('meet-backend', resource_deps=['postgresql', 'garage-cors', 'redis', 'livekit-livekit-server'])
|
||||
k8s_resource('minio-bucket', resource_deps=['minio'])
|
||||
k8s_resource('meet-backend', resource_deps=['postgresql', 'minio', 'redis', 'livekit-livekit-server'])
|
||||
k8s_resource('meet-celery-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summarize', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-summary-backend', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-transcribe-default', resource_deps=['redis'])
|
||||
k8s_resource('meet-celery-transcribe', resource_deps=['redis'])
|
||||
k8s_resource('meet-backend-migrate', resource_deps=['meet-backend'])
|
||||
k8s_resource('livekit-livekit-server', resource_deps=['redis'])
|
||||
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
|
||||
k8s_resource('keycloak', resource_deps=['kc-postgresql'])
|
||||
# Trigger once on launch
|
||||
k8s_resource(
|
||||
'meet-backend-createsuperuser',
|
||||
resource_deps=['meet-backend-migrate'],
|
||||
trigger_mode=TRIGGER_MODE_MANUAL,
|
||||
)
|
||||
k8s_resource(
|
||||
'meet-backend-migrate',
|
||||
resource_deps=['meet-backend'],
|
||||
trigger_mode=TRIGGER_MODE_MANUAL,
|
||||
)
|
||||
k8s_resource('meet-backend-createsuperuser', resource_deps=['meet-backend-migrate'])
|
||||
|
||||
migration = '''
|
||||
set -eu
|
||||
|
||||
@@ -5,7 +5,7 @@ set -eo pipefail
|
||||
REPO_DIR="$(cd "$( dirname "${BASH_SOURCE[0]}" )/.." && pwd)"
|
||||
UNSET_USER=0
|
||||
|
||||
export COMPOSE_FILE="${COMPOSE_FILE:-${REPO_DIR}/compose.yml}"
|
||||
COMPOSE_FILE="${REPO_DIR}/compose.yml"
|
||||
COMPOSE_PROJECT="meet"
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ function _docker_compose() {
|
||||
echo "🐳(compose) project: '${COMPOSE_PROJECT}' file: '${COMPOSE_FILE}'"
|
||||
docker compose \
|
||||
-p "${COMPOSE_PROJECT}" \
|
||||
-f "${COMPOSE_FILE}" \
|
||||
--project-directory "${REPO_DIR}" \
|
||||
"$@"
|
||||
}
|
||||
|
||||
@@ -47,3 +47,4 @@ mv src/backend/* ./
|
||||
mv deploy/paas/* ./
|
||||
|
||||
echo "3.13" > .python-version
|
||||
echo "." > requirements.txt
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# shellcheck source=bin/_config.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/_config.sh"
|
||||
|
||||
_docker_compose "$@"
|
||||
@@ -101,30 +101,12 @@ update_npm_version "mail"
|
||||
# Update backend pyproject.toml
|
||||
update_python_version "backend"
|
||||
|
||||
# Run uv lock in backend
|
||||
print_info "Running uv lock in backend..."
|
||||
cd "src/backend"
|
||||
uv lock
|
||||
cd -
|
||||
|
||||
# Update summary pyproject.toml
|
||||
update_python_version "summary"
|
||||
|
||||
# Run uv lock in summary
|
||||
print_info "Running uv lock in summary..."
|
||||
cd "src/summary"
|
||||
uv lock
|
||||
cd -
|
||||
|
||||
# Update agents pyproject.toml
|
||||
update_python_version "agents"
|
||||
|
||||
# Run uv lock in agents
|
||||
print_info "Running uv lock in agents..."
|
||||
cd "src/agents"
|
||||
uv lock
|
||||
cd -
|
||||
|
||||
# Update CHANGELOG
|
||||
print_info "Updating CHANGELOG..."
|
||||
|
||||
@@ -167,11 +149,8 @@ echo " - src/frontend/package.json"
|
||||
echo " - src/sdk/package.json"
|
||||
echo " - src/mail/package.json"
|
||||
echo " - src/backend/pyproject.toml"
|
||||
echo " - src/backend/uv.lock"
|
||||
echo " - src/summary/pyproject.toml"
|
||||
echo " - src/summary/uv.lock"
|
||||
echo " - src/agents/pyproject.toml"
|
||||
echo " - src/agents/uv.lock"
|
||||
echo " - CHANGELOG.md"
|
||||
echo ""
|
||||
print_warning "Next steps:"
|
||||
|
||||
@@ -15,44 +15,51 @@ services:
|
||||
ports:
|
||||
- "1081:1080"
|
||||
|
||||
garage:
|
||||
minio:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: dxflrs/garage:v2.4.1
|
||||
command: /garage server --single-node --default-bucket
|
||||
env_file:
|
||||
- env.d/development/garage
|
||||
image: minio/minio
|
||||
environment:
|
||||
- GARAGE_DEFAULT_ACCESS_KEY=meet-access-key
|
||||
- GARAGE_DEFAULT_SECRET_KEY=meet-secret-access-key
|
||||
- GARAGE_DEFAULT_BUCKET=meet-media-storage
|
||||
- MINIO_ROOT_USER=meet
|
||||
- MINIO_ROOT_PASSWORD=password
|
||||
ports:
|
||||
- '127.0.0.1:9000:9000'
|
||||
- '9000:9000'
|
||||
- '9001:9001'
|
||||
healthcheck:
|
||||
test: [ "CMD", "/garage", "health" ]
|
||||
test: [ "CMD", "mc", "ready", "local" ]
|
||||
interval: 1s
|
||||
timeout: 20s
|
||||
retries: 300
|
||||
entrypoint: ""
|
||||
command: minio server --console-address :9001 /data
|
||||
volumes:
|
||||
- ./docker/files/etc/garage/garage.toml:/etc/garage.toml:ro
|
||||
- ./data/media:/var/lib/garage
|
||||
- ./data/media:/data
|
||||
|
||||
# Garage denies cross-origin requests by default: allow the frontend to upload files
|
||||
garage-cors:
|
||||
image: amazon/aws-cli:2.37.1
|
||||
environment:
|
||||
- AWS_ACCESS_KEY_ID=meet-access-key
|
||||
- AWS_SECRET_ACCESS_KEY=meet-secret-access-key
|
||||
- AWS_DEFAULT_REGION=local
|
||||
createbuckets:
|
||||
image: minio/mc
|
||||
depends_on:
|
||||
garage:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
command:
|
||||
- s3api
|
||||
- put-bucket-cors
|
||||
- --endpoint-url=http://garage:9000
|
||||
- --bucket=meet-media-storage
|
||||
- '--cors-configuration={"CORSRules": [{"AllowedOrigins": ["http://localhost:3000"], "AllowedMethods": ["GET", "HEAD", "PUT"], "AllowedHeaders": ["*"], "ExposeHeaders": ["ETag"]}]}'
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
/usr/bin/mc alias set meet http://minio:9000 meet password && \
|
||||
/usr/bin/mc mb meet/meet-media-storage && \
|
||||
exit 0;"
|
||||
|
||||
createwebhook:
|
||||
image: minio/mc
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
entrypoint: >
|
||||
sh -c "
|
||||
/usr/bin/mc alias set meet http://minio:9000 meet password &&
|
||||
/usr/bin/mc admin config set meet notify_webhook:meet-webhook endpoint='http://app-dev:8000/api/v1.0/recordings/storage-hook/' auth_token='Bearer password' &&
|
||||
/usr/bin/mc admin service restart meet --wait --json &&
|
||||
sleep 15 &&
|
||||
/usr/bin/mc event add meet/meet-media-storage arn:minio:sqs::meet-webhook:webhook --event put --prefix "recordings" &&
|
||||
exit 0;"
|
||||
|
||||
app-dev:
|
||||
build:
|
||||
@@ -78,17 +85,19 @@ services:
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- redis
|
||||
- garage-cors
|
||||
- livekit
|
||||
- createbuckets
|
||||
- createwebhook
|
||||
extra_hosts:
|
||||
- "127.0.0.1.nip.io:host-gateway"
|
||||
networks:
|
||||
- resource-server
|
||||
- default
|
||||
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: meet:backend-development
|
||||
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "DEBUG", "--pool=solo"]
|
||||
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "DEBUG"]
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=Development
|
||||
env_file:
|
||||
@@ -118,12 +127,12 @@ services:
|
||||
- postgresql
|
||||
- redis
|
||||
- livekit
|
||||
- garage
|
||||
- minio
|
||||
|
||||
celery:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
image: meet:backend-production
|
||||
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "INFO", "--pool=solo"]
|
||||
command: ["celery", "-A", "meet.celery_app", "worker", "-l", "INFO"]
|
||||
environment:
|
||||
- DJANGO_CONFIGURATION=Demo
|
||||
env_file:
|
||||
@@ -172,7 +181,7 @@ services:
|
||||
working_dir: /app
|
||||
|
||||
node:
|
||||
image: node:22
|
||||
image: node:18
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
environment:
|
||||
HOME: /tmp
|
||||
@@ -215,14 +224,12 @@ services:
|
||||
- kc_postgresql
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:v1.13.6
|
||||
image: livekit/livekit-server
|
||||
entrypoint: /livekit-server --dev --bind 0.0.0.0 --config ./config.yaml
|
||||
ports:
|
||||
- "7880:7880"
|
||||
- "7881:7881"
|
||||
- "7882:7882/udp"
|
||||
- "3478:3478/udp"
|
||||
- "30000-30100:30000-30100/udp"
|
||||
volumes:
|
||||
- ./docker/livekit/config/livekit-server.yaml:/config.yaml
|
||||
depends_on:
|
||||
@@ -230,45 +237,14 @@ 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
|
||||
|
||||
metadata-collector-dev:
|
||||
build:
|
||||
context: ./src/agents
|
||||
target: development
|
||||
user: ${DOCKER_USER:-1000}
|
||||
command: ["python", "metadata_collector.py", "dev"]
|
||||
env_file:
|
||||
- env.d/development/metadata_collector
|
||||
volumes:
|
||||
- ./src/agents:/app
|
||||
- /app/.venv
|
||||
depends_on:
|
||||
- livekit
|
||||
- garage
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ./src/agents
|
||||
|
||||
multi-user-transcriber-dev:
|
||||
build:
|
||||
context: ./src/agents
|
||||
target: development
|
||||
user: ${DOCKER_USER:-1000}
|
||||
command: ["python", "multi_user_transcriber.py", "dev"]
|
||||
env_file:
|
||||
- env.d/development/multi_user_transcriber
|
||||
volumes:
|
||||
- ./src/agents:/app
|
||||
- /app/.venv
|
||||
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
|
||||
|
||||
redis-summary:
|
||||
image: redis
|
||||
@@ -297,7 +273,7 @@ services:
|
||||
context: ./src/summary
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q transcribe-queue-v2
|
||||
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q transcribe-queue
|
||||
env_file:
|
||||
- env.d/development/summary
|
||||
volumes:
|
||||
@@ -305,7 +281,7 @@ services:
|
||||
depends_on:
|
||||
- redis-summary
|
||||
- app-summary-dev
|
||||
- garage
|
||||
- minio
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
@@ -317,7 +293,7 @@ services:
|
||||
context: ./src/summary
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q summarize-queue-v2
|
||||
command: celery -A summary.core.celery_worker worker --pool=solo --loglevel=debug -Q summarize-queue
|
||||
env_file:
|
||||
- env.d/development/summary
|
||||
volumes:
|
||||
@@ -325,7 +301,7 @@ services:
|
||||
depends_on:
|
||||
- redis-summary
|
||||
- app-summary-dev
|
||||
- garage
|
||||
- minio
|
||||
develop:
|
||||
watch:
|
||||
- action: rebuild
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1778705847,
|
||||
"narHash": "sha256-EQnZCy7r4VMO6KDoytxHBa0mFbM1D9g1kaDfs/s0YZA=",
|
||||
"ref": "refs/tags/v2.1.2",
|
||||
"rev": "ea3d94ac9d6bf6a1313773170122ca4e2ef5a0be",
|
||||
"revCount": 6569,
|
||||
"type": "git",
|
||||
"url": "https://github.com/cachix/devenv"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"ref": "refs/tags/v2.1.2",
|
||||
"type": "git",
|
||||
"url": "https://github.com/cachix/devenv"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1789542786,
|
||||
"narHash": "sha256-ajQuqcxnj6RYnwCjA/9FTCnrdR2+BDzstE8UembXf60=",
|
||||
"ref": "nixos-26.05",
|
||||
"rev": "4c7870105e7f1fdf9c48688c8d7efc21abf0688a",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NixOS/nixpkgs"
|
||||
},
|
||||
"original": {
|
||||
"ref": "nixos-26.05",
|
||||
"shallow": true,
|
||||
"type": "git",
|
||||
"url": "https://github.com/NixOS/nixpkgs"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,265 +0,0 @@
|
||||
# =============================================================================
|
||||
# devenv.nix — La Suite Meet ("Visio") developer environment
|
||||
# =============================================================================
|
||||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
python = pkgs.python313;
|
||||
nodejs = pkgs.nodejs_22;
|
||||
|
||||
backendDir = "src/backend";
|
||||
agentsDir = "src/agents";
|
||||
summaryDir = "src/summary";
|
||||
frontendDir = "src/frontend";
|
||||
|
||||
readDotEnv =
|
||||
file:
|
||||
let
|
||||
lines = lib.splitString "\n" (builtins.readFile file);
|
||||
unquote =
|
||||
v:
|
||||
let
|
||||
len = builtins.stringLength v;
|
||||
in
|
||||
if len >= 2 && lib.hasPrefix "\"" v && lib.hasSuffix "\"" v then
|
||||
builtins.substring 1 (len - 2) v
|
||||
else if len >= 2 && lib.hasPrefix "'" v && lib.hasSuffix "'" v then
|
||||
builtins.substring 1 (len - 2) v
|
||||
else
|
||||
v;
|
||||
parseLine =
|
||||
line:
|
||||
let
|
||||
m = builtins.match "[ \t]*([A-Za-z_][A-Za-z0-9_]*)[ \t]*=[ \t]*(.*)" line;
|
||||
in
|
||||
if m == null then null else { name = builtins.elemAt m 0; value = unquote (builtins.elemAt m 1); };
|
||||
in
|
||||
builtins.listToAttrs (builtins.filter (x: x != null) (map parseLine lines));
|
||||
|
||||
# Reuse existing .env
|
||||
dotEnv =
|
||||
(readDotEnv ./env.d/development/common.dist)
|
||||
// (readDotEnv ./env.d/development/postgresql.dist);
|
||||
|
||||
sharedEnv = builtins.removeAttrs dotEnv [ "PYTHONPATH" ]; # only makes sense inside the backend container.
|
||||
in
|
||||
{
|
||||
options.meet = {
|
||||
agents.enable = lib.mkEnableOption "tooling for the LiveKit agents in src/agents";
|
||||
summary.enable = lib.mkEnableOption "tooling for the summary service in src/summary";
|
||||
k8s.enable = lib.mkEnableOption "Kubernetes dev utilities";
|
||||
};
|
||||
|
||||
config = {
|
||||
# Profile can be activated with devenv --profile <profile> shell
|
||||
profiles = {
|
||||
agents.module = {
|
||||
meet.agents.enable = true;
|
||||
};
|
||||
summary.module = {
|
||||
meet.summary.enable = true;
|
||||
};
|
||||
k8s.module = {
|
||||
meet.k8s.enable = true;
|
||||
};
|
||||
};
|
||||
languages.python = {
|
||||
enable = true;
|
||||
package = python;
|
||||
directory = backendDir;
|
||||
manylinux.enable = pkgs.stdenv.hostPlatform.isLinux;
|
||||
|
||||
libraries = [
|
||||
"${config.devenv.dotfile}/profile"
|
||||
pkgs.file
|
||||
pkgs.zlib
|
||||
pkgs.libffi
|
||||
pkgs.openssl
|
||||
];
|
||||
|
||||
uv.enable = true;
|
||||
uv.sync.enable = false;
|
||||
venv.enable = false;
|
||||
lsp.enable = true;
|
||||
};
|
||||
|
||||
languages.javascript = {
|
||||
enable = true;
|
||||
package = nodejs;
|
||||
directory = frontendDir;
|
||||
|
||||
npm.enable = true;
|
||||
yarn.enable = true;
|
||||
corepack.enable = false;
|
||||
};
|
||||
|
||||
languages.typescript.enable = false;
|
||||
languages.nix.enable = true;
|
||||
|
||||
packages =
|
||||
with pkgs;
|
||||
[
|
||||
gnumake
|
||||
file
|
||||
shared-mime-info
|
||||
gettext
|
||||
postgresql_16
|
||||
git
|
||||
curl
|
||||
jq
|
||||
podman
|
||||
podman-compose
|
||||
docker-client
|
||||
]
|
||||
|
||||
# -- LiveKit agents
|
||||
++ lib.optionals config.meet.agents.enable [
|
||||
glib
|
||||
portaudio
|
||||
livekit-cli
|
||||
]
|
||||
|
||||
# -- summary service
|
||||
++ lib.optionals config.meet.summary.enable [
|
||||
redis
|
||||
]
|
||||
|
||||
# -- Kubernetes tools
|
||||
++ lib.optionals config.meet.k8s.enable [
|
||||
kubectl
|
||||
kubernetes-helm
|
||||
helmfile
|
||||
tilt
|
||||
kind
|
||||
mkcert
|
||||
];
|
||||
|
||||
env = sharedEnv // {
|
||||
UV_LINK_MODE = "copy";
|
||||
|
||||
PYTHONDONTWRITEBYTECODE = "1";
|
||||
PYTHONUNBUFFERED = "1";
|
||||
|
||||
UV_PROJECT_ENVIRONMENT = lib.mkForce ".venv";
|
||||
|
||||
COMPOSE_PROJECT_NAME = "meet";
|
||||
|
||||
DJANGO_DATA_DIR = "${config.devenv.root}/data";
|
||||
|
||||
# Database / Pgsql
|
||||
DB_HOST = "127.0.0.1";
|
||||
DB_PORT = "15432";
|
||||
PGHOST = "127.0.0.1";
|
||||
PGPORT = "15432";
|
||||
PGDATABASE = sharedEnv.DB_NAME;
|
||||
PGUSER = sharedEnv.DB_USER;
|
||||
PGPASSWORD = sharedEnv.DB_PASSWORD;
|
||||
|
||||
REDIS_URL = "redis://127.0.0.1:6379/1";
|
||||
CELERY_BROKER_URL = "redis://127.0.0.1:6379/0";
|
||||
|
||||
# S3 / Garage
|
||||
AWS_S3_ENDPOINT_URL = "http://127.0.0.1:9000";
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/certs";
|
||||
OIDC_OP_TOKEN_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/token";
|
||||
OIDC_OP_USER_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/userinfo";
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT = "http://localhost:8083/realms/meet/protocol/openid-connect/token/introspect";
|
||||
|
||||
# summary service
|
||||
SUMMARY_SERVICE_ENDPOINT = "http://127.0.0.1:8001/api/v2/async-jobs/transcribe/";
|
||||
SUMMARY_SERVICE_VERSION = "2";
|
||||
|
||||
# Mail
|
||||
DJANGO_EMAIL_HOST = "127.0.0.1";
|
||||
};
|
||||
|
||||
scripts = {
|
||||
|
||||
meet-venv = {
|
||||
description = "Create/refresh meet uv virtualenvs for backend, agents and summary";
|
||||
exec = ''
|
||||
set -euo pipefail
|
||||
cd "$DEVENV_ROOT"
|
||||
|
||||
echo "==> ${backendDir} (uv sync --locked, dependency-groups)"
|
||||
( cd "${backendDir}" && uv sync --locked --all-groups )
|
||||
|
||||
echo "==> ${agentsDir} (uv sync --locked --all-extras)"
|
||||
( cd "${agentsDir}" && uv sync --locked --all-extras )
|
||||
|
||||
echo "==> ${summaryDir} (uv sync --locked --all-extras)"
|
||||
( cd "${summaryDir}" && uv sync --locked --all-extras )
|
||||
|
||||
echo
|
||||
echo "Synced the following virtualenvs successfully:"
|
||||
echo " ${backendDir}/.venv"
|
||||
echo " ${agentsDir}/.venv"
|
||||
echo " ${summaryDir}/.venv"
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
enterShell = ''
|
||||
# Make podman socket accessible in order to launch regular docker commands.
|
||||
# Set MEET_PODMAN_SOCKET=0 to keep the DOCKER_HOST of the calling environment.
|
||||
case "''${MEET_PODMAN_SOCKET:-1}" in
|
||||
0|false|no|off) ;;
|
||||
*)
|
||||
_rundir="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
export DOCKER_HOST="unix://$_rundir/podman/podman.sock"
|
||||
unset _rundir
|
||||
;;
|
||||
esac
|
||||
|
||||
# Compose files to merge
|
||||
_compose_dir="${config.devenv.root}/docker/compose.d"
|
||||
_compose_files="${config.devenv.root}/compose.yml"
|
||||
|
||||
export DOCKER_USER="$(id -u):$(id -g)"
|
||||
|
||||
case "''${DOCKER_HOST:-}" in
|
||||
*podman*)
|
||||
_compose_files="$_compose_files:$_compose_dir/compose.podman.yml"
|
||||
|
||||
# Build images with Podman/Buildah rather than BuildKit. `docker
|
||||
# compose build` otherwise has buildx boot a moby/buildkit container,
|
||||
# and that container lands in its own network namespace with neither
|
||||
# the proxy in its environment nor any route to it.
|
||||
# Buildah has neither problem: base images are resolved by the Podman systemd
|
||||
# service, which inherits the proxy from its systemd socket activated unit, and
|
||||
# RUN steps execute in the *host* network namespace
|
||||
|
||||
export DOCKER_BUILDKIT=0
|
||||
export COMPOSE_BAKE=false
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
# Apply Bureautix override
|
||||
if [ -n "''${http_proxy:-}" ]; then
|
||||
_compose_files="$_compose_files:$_compose_dir/compose.bureautix.yml"
|
||||
fi
|
||||
|
||||
export COMPOSE_FILE="$_compose_files"
|
||||
unset _compose_dir _compose_files
|
||||
|
||||
# Make binaries accessible
|
||||
for _d in \
|
||||
"$DEVENV_ROOT/${backendDir}/.venv/bin" \
|
||||
"$DEVENV_ROOT/${agentsDir}/.venv/bin" \
|
||||
"$DEVENV_ROOT/${summaryDir}/.venv/bin" \
|
||||
"$DEVENV_ROOT/${frontendDir}/node_modules/.bin"
|
||||
do
|
||||
[ -d "$_d" ] && export PATH="$_d:$PATH"
|
||||
done
|
||||
unset _d
|
||||
'';
|
||||
};
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: git+https://github.com/NixOS/nixpkgs?ref=nixos-26.05&shallow=1
|
||||
devenv:
|
||||
url: git+https://github.com/cachix/devenv?ref=refs/tags/v2.1.2&dir=src/modules
|
||||
@@ -60,7 +60,7 @@
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-chromium",
|
||||
"email": "user@chromium.e2e",
|
||||
"email": "user.test@chromium.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Chromium",
|
||||
"enabled": "true",
|
||||
@@ -74,7 +74,7 @@
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-webkit",
|
||||
"email": "user@webkit.e2e",
|
||||
"email": "user.test@webkit.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Webkit",
|
||||
"enabled": "true",
|
||||
@@ -88,7 +88,7 @@
|
||||
},
|
||||
{
|
||||
"username": "user-e2e-firefox",
|
||||
"email": "user@firefox.e2e",
|
||||
"email": "user.test@firefox.test",
|
||||
"firstName": "E2E",
|
||||
"lastName": "Firefox",
|
||||
"enabled": "true",
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Bureautix proxy overrides
|
||||
#
|
||||
# Builds submitted through the Docker API of the Podman service get none of
|
||||
# the proxy variables in their RUN steps. We need to pass the http_proxy explicitely
|
||||
# otherwise all connections fail during the build.
|
||||
|
||||
x-proxy-vars: &proxy-vars
|
||||
http_proxy: ${http_proxy:-}
|
||||
https_proxy: ${https_proxy:-}
|
||||
no_proxy: ${no_proxy:-}
|
||||
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
app-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
frontend:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
metadata-collector-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
multi-user-transcriber-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
app-summary-dev:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
celery-summary-transcribe:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
celery-summary-summarize:
|
||||
build:
|
||||
args:
|
||||
<<: *proxy-vars
|
||||
# The local proxy listens on 8080 and collides with Keycloak's published admin port.
|
||||
keycloak:
|
||||
ports: !override
|
||||
- "8081:8080"
|
||||
@@ -1,35 +0,0 @@
|
||||
# Rootless Podman override for compose.yml.
|
||||
#
|
||||
# Rootless Podman maps container UID 0 to the host user and every other
|
||||
# container UID to a subuid that owns nothing in the worktree. Meet compose.ymlruns
|
||||
# its containers as DOCKER_USER=$(id -u):$(id -g), which would land on such a
|
||||
# subuid and make every bind mount effectively read-only.
|
||||
#
|
||||
# `userns_mode: keep-id` maps the host user to the same UID and GID inside the
|
||||
# container instead, so DOCKER_USER keeps its Docker value and files written
|
||||
# through a bind mount are owned by the host user on both sides.
|
||||
#
|
||||
# Only the services that mount the worktree and run as DOCKER_USER are listed.
|
||||
|
||||
x-keep-id: &keep-id
|
||||
userns_mode: keep-id
|
||||
|
||||
services:
|
||||
app-dev:
|
||||
<<: *keep-id
|
||||
celery-dev:
|
||||
<<: *keep-id
|
||||
garage:
|
||||
<<: *keep-id
|
||||
garage-cors:
|
||||
<<: *keep-id
|
||||
node:
|
||||
<<: *keep-id
|
||||
crowdin:
|
||||
<<: *keep-id
|
||||
metadata-collector-dev:
|
||||
<<: *keep-id
|
||||
multi-user-transcriber-dev:
|
||||
<<: *keep-id
|
||||
app-summary-dev:
|
||||
<<: *keep-id
|
||||
@@ -1,5 +1,5 @@
|
||||
# ---- Front-end image ----
|
||||
FROM node:22-alpine AS frontend-deps
|
||||
FROM node:20-alpine AS frontend-deps
|
||||
|
||||
WORKDIR /home/frontend/
|
||||
|
||||
@@ -38,28 +38,16 @@ COPY ./docker/dinum-frontend/assets/ \
|
||||
COPY ./docker/dinum-frontend/fonts/ \
|
||||
./dist/assets/fonts/
|
||||
|
||||
# ---- Addons builder image ----
|
||||
FROM node:20-alpine AS addons-builder
|
||||
|
||||
WORKDIR /home/addons/outlook
|
||||
|
||||
COPY ./src/addons/outlook/package.json ./package.json
|
||||
COPY ./src/addons/outlook/package-lock.json ./package-lock.json
|
||||
|
||||
RUN npm ci
|
||||
|
||||
COPY ./src/addons/outlook/ .
|
||||
|
||||
RUN npx webpack --mode production
|
||||
|
||||
|
||||
# ---- Front-end image ----
|
||||
FROM nginxinc/nginx-unprivileged:1.30.4-alpine3.24 AS frontend-production
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk upgrade --no-cache libexpat && \
|
||||
apk del curl
|
||||
USER nginx
|
||||
RUN apk update && apk upgrade libssl3 \
|
||||
libcrypto3 \
|
||||
libxml2>=2.12.7-r2 \
|
||||
libxslt>=1.1.39-r2 \
|
||||
libexpat>=2.7.2-r0 \
|
||||
libpng>=1.6.53-r0
|
||||
|
||||
USER nginx
|
||||
|
||||
@@ -71,11 +59,7 @@ COPY --from=meet-builder \
|
||||
/home/frontend/dist \
|
||||
/usr/share/nginx/html
|
||||
|
||||
COPY --from=addons-builder \
|
||||
/home/addons/outlook/dist \
|
||||
/usr/share/nginx/html/addons/outlook
|
||||
|
||||
COPY ./docker/dinum-frontend/nginx/default.conf /etc/nginx/conf.d
|
||||
COPY ./src/frontend/default.conf /etc/nginx/conf.d
|
||||
COPY ./docker/files/usr/local/bin/entrypoint /usr/local/bin/entrypoint
|
||||
|
||||
ENTRYPOINT [ "/usr/local/bin/entrypoint" ]
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
:root {
|
||||
--fonts-sans: 'Marianne', ui-sans-serif, system-ui, sans-serif;
|
||||
--avatar-cap-height: 0.7;
|
||||
}
|
||||
|
||||
.Header-beforeLogo {
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
location ^~ /assets/mediapipe/wasm/ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000";
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
# Garage configuration for local development only: single node, no replication.
|
||||
# See https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
|
||||
metadata_dir = "/var/lib/garage/meta"
|
||||
data_dir = "/var/lib/garage/data"
|
||||
db_engine = "lmdb"
|
||||
|
||||
replication_factor = 1
|
||||
|
||||
rpc_bind_addr = "127.0.0.1:3901"
|
||||
rpc_public_addr = "127.0.0.1:3901"
|
||||
|
||||
[s3_api]
|
||||
api_bind_addr = "[::]:9000"
|
||||
# Clients must sign their requests for this region (AWS_S3_REGION_NAME)
|
||||
s3_region = "local"
|
||||
@@ -17,9 +17,9 @@ server {
|
||||
proxy_set_header X-Amz-Date $authDate;
|
||||
proxy_set_header X-Amz-Content-SHA256 $authContentSha256;
|
||||
|
||||
# Get resource from Garage
|
||||
proxy_pass http://garage:9000/meet-media-storage/;
|
||||
proxy_set_header Host garage:9000;
|
||||
# Get resource from Minio
|
||||
proxy_pass http://minio:9000/meet-media-storage/;
|
||||
proxy_set_header Host minio:9000;
|
||||
# To use with ds_proxy
|
||||
# proxy_pass http://ds-proxy:4444/upstream/meet-media-storage/;
|
||||
# proxy_set_header Host ds-proxy:4444;
|
||||
|
||||
@@ -14,4 +14,3 @@ accesslog = "-"
|
||||
# Using '-' for the error log file makes gunicorn log errors to stderr
|
||||
errorlog = "-"
|
||||
loglevel = "info"
|
||||
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(M)s'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM livekit/livekit-server:v1.13.6
|
||||
FROM livekit/livekit-server:v1.9.4
|
||||
|
||||
# We inject the nip.io certificate manually because the livekit chart doesn't support volume mounting
|
||||
COPY rootCA.pem /etc/ssl/certs/
|
||||
|
||||
@@ -8,23 +8,3 @@ webhook:
|
||||
api_key: devkey
|
||||
urls:
|
||||
- http://app-dev:8000/api/v1.0/rooms/webhooks-livekit/
|
||||
|
||||
turn:
|
||||
enabled: true
|
||||
domain: turn.127.0.0.1.nip.io
|
||||
udp_port: 3478
|
||||
tls_port: 0
|
||||
external_tls: false
|
||||
relay_range_start: 30000
|
||||
relay_range_end: 30100
|
||||
allow_restricted_peer_cidrs:
|
||||
- 192.168.0.0/16
|
||||
- 172.16.0.0/12
|
||||
|
||||
rtc:
|
||||
node_ip: 127.0.0.1
|
||||
advertise_internal_ip: true
|
||||
udp_port: 7882
|
||||
tcp_port: 7881
|
||||
use_external_ip: false
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ These components rely on a few key services:
|
||||
|
||||
- PostgreSQL for storing data (users, rooms, recordings)
|
||||
- Redis for caching and inter-service communication
|
||||
- Garage for storing files (room recordings)
|
||||
- MinIO for storing files (room recordings)
|
||||
- Celery workers for meeting transcript (optional, required for AI beta features)
|
||||
|
||||
We provide two stack options for getting Visio up and running for development:
|
||||
|
||||
@@ -23,11 +23,14 @@ It uses LiveKit Egress to record room sessions. For reference, see the [LiveKit
|
||||
To use the room recording feature, the following components are required:
|
||||
|
||||
- A running [LiveKit Egress](https://github.com/livekit/egress) server capable of handling room composite recordings.
|
||||
- A S3-compatible object storage where the egress uploads the recorded files.
|
||||
- A S3-compatible object storage that supports webhook events to notify the backend when recordings are uploaded.
|
||||
- An email service to notify room owners when a recording is available for download.
|
||||
- Webhook events configured between LiveKit Server and the backend.
|
||||
|
||||
|
||||
> [!CAUTION]
|
||||
> Minio supports lifecycle events; other providers may not work out of the box. There is currently a dependency on Minio, which is planned to be refactored in the future.
|
||||
|
||||
> [!NOTE]
|
||||
> Celery isn’t in use for these async tasks yet. It’s something we’d like to add, but it’s not planned at this stage.
|
||||
|
||||
@@ -72,7 +75,7 @@ sequenceDiagram
|
||||
LiveKit->>Egress: Stop recording
|
||||
Egress->>Storage: Upload recorded file
|
||||
|
||||
LiveKit->>Backend: POST /api/v1.0/rooms/webhooks-livekit/ (egress_ended)
|
||||
Storage->>Backend: Storage event notification
|
||||
Backend->>Backend: Update Recording status to SAVED
|
||||
Backend->>Email: Send notification to room owner
|
||||
|
||||
@@ -91,17 +94,27 @@ sequenceDiagram
|
||||
| **RECORDING_ENABLE** | Boolean | `False` | Enable or disable the room recording feature. |
|
||||
| **RECORDING_OUTPUT_FOLDER** | String | `"recordings"` | Folder/prefix where recordings are stored in the object storage. |
|
||||
| **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_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. |
|
||||
| **RECORDING_ENCODING_ENABLED** | Boolean | `False` | When `False`, LiveKit Egress uses its built-in `H264_720P_30` preset. When `True`, the `RECORDING_ENCODING_*` values below are sent to LiveKit as advanced `EncodingOptions`. See [Tuning recording encoding](#tuning-recording-encoding). |
|
||||
| **RECORDING_ENCODING_WIDTH** | Integer | `1280` | Recording video width in pixels. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_HEIGHT** | Integer | `720` | Recording video height in pixels. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_FRAMERATE** | Integer | `30` | Recording video framerate (fps). Directly impacts egress worker CPU (roughly linear). Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_VIDEO_BITRATE_KBPS** | Integer | `3000` | H.264 MAIN video bitrate in kbps. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_AUDIO_BITRATE_KBPS** | Integer | `128` | AAC audio bitrate in kbps. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
| **RECORDING_ENCODING_KEY_FRAME_INTERVAL_S** | Float | `4.0` | Keyframe interval in seconds. Drives seek granularity in the recorded MP4 (a player can only seek to keyframe boundaries). Larger values give the encoder slightly more bits for non-keyframe content at a fixed bitrate. `4.0` is a standard VOD value. Only applied when `RECORDING_ENCODING_ENABLED` is `True`. |
|
||||
|
||||
|
||||
### Manual Storage Webhook
|
||||
|
||||
Storage events must be configured manually; the Kubernetes chart does not do this automatically.
|
||||
|
||||
1. Configure your S3 bucket to send file creation events to the backend webhook.
|
||||
2. Enable events and token in settings:
|
||||
|
||||
```python
|
||||
RECORDING_STORAGE_EVENT_ENABLE = True
|
||||
RECORDING_ENABLE_STORAGE_EVENT_AUTH = True
|
||||
RECORDING_STORAGE_EVENT_TOKEN = <token>
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> Questions? Open an issue on [GitHub](https://github.com/suitenumerique/meet/issues/new?assignees=&labels=bug&template=Bug_report.md) or join our [Matrix community](https://matrix.to/#/#meet-official:matrix.org).
|
||||
|
||||
@@ -128,54 +141,3 @@ Using default project meet
|
||||
|
||||
This allows you to verify which recordings are in progress, troubleshoot egress issues, and confirm that recordings are being processed correctly.
|
||||
|
||||
## Tuning recording encoding
|
||||
|
||||
By default, LiveKit Egress records with the built-in `H264_720P_30` preset: 1280×720 at 30 fps, 3000 kbps H.264 MAIN video and 128 kbps AAC audio. For a one-hour meeting this produces a file of roughly **1.4 GB**, which is often heavier than necessary for talking-head content and screen sharing.
|
||||
|
||||
The `RECORDING_ENCODING_*` settings let operators override this preset without modifying the source. Values are passed straight through LiveKit's `EncodingOptions.advanced` to the GStreamer pipeline (`x264enc` for video, `faac` for audio), so there are no hidden conversions — what you set is what the encoder receives.
|
||||
|
||||
### How values map to GStreamer
|
||||
|
||||
| Setting | GStreamer element | Property |
|
||||
| ------------------------------------- | ----------------- | ---------------------------------- |
|
||||
| `RECORDING_ENCODING_WIDTH/HEIGHT` | capsfilter | `video/x-raw,width=W,height=H` |
|
||||
| `RECORDING_ENCODING_FRAMERATE` | capsfilter | `framerate=F/1` |
|
||||
| `RECORDING_ENCODING_VIDEO_BITRATE_KBPS` | `x264enc` | `bitrate=kbps` (kilobits) |
|
||||
| `RECORDING_ENCODING_KEY_FRAME_INTERVAL_S` | `x264enc` | `key-int-max = interval × fps` |
|
||||
| `RECORDING_ENCODING_AUDIO_BITRATE_KBPS` | `faac` | `bitrate = kbps × 1000` (bits) |
|
||||
|
||||
The H.264 profile is fixed to MAIN and the x264 `speed-preset` to `veryfast` by LiveKit (real-time constraint) — lowering the framerate is therefore the main lever to save CPU, while lowering the bitrate is the main lever to shrink the output file.
|
||||
|
||||
### Reference profiles
|
||||
|
||||
Rough 30-minute file-size estimates assume video + audio bitrate multiplied by duration. Actual sizes vary with content (static talking heads compress better than heavy screen motion). Egress CPU figures are indicative, measured on a single Ryzen laptop core saturated by the default preset (= 100 %); scaling is roughly linear with `framerate × bitrate` but the absolute numbers depend on the host hardware.
|
||||
|
||||
| Profile | Resolution | FPS | Video (kbps) | Audio (kbps) | Keyframe (s) | ~ size / 30 min | Egress CPU (vs. default) | Suitable for |
|
||||
| ---------------------- | ---------- | --- | ------------ | ------------ | ------------ | --------------- | ------------------------ | --------------------------------------------------- |
|
||||
| Default (preset) | 1280×720 | 30 | 3000 | 128 | 4 | **~690 MB** | 100 % | Unchanged LiveKit behaviour |
|
||||
| Balanced | 1280×720 | 20 | 1000 | 96 | 4 | ~240 MB | ~67 % | Mixed content, moderate motion |
|
||||
| **Low CPU / small file** | 1280×720 | 15 | 600 | 64 | 4 | **~150 MB** | ~50 % | Talking-head dominant meetings + occasional slides ★ |
|
||||
| Slide-heavy | 1280×720 | 15 | 900 | 64 | 4 | ~210 MB | ~55 % | Frequent dense screen sharing (decks, IDE, docs) |
|
||||
| Minimum CPU | 960×540 | 15 | 500 | 64 | 4 | ~125 MB | ~30 % | Voice-first meetings, readable text not required |
|
||||
| Audio-heavy fallback | 1280×720 | 10 | 400 | 96 | 4 | ~110 MB | ~35 % | Long webinars, low motion |
|
||||
|
||||
★ Recommended starting point for typical LaSuite Meet usage.
|
||||
|
||||
Environment variables for the **Low CPU / small file** profile:
|
||||
|
||||
```bash
|
||||
RECORDING_ENCODING_ENABLED=True
|
||||
RECORDING_ENCODING_WIDTH=1280
|
||||
RECORDING_ENCODING_HEIGHT=720
|
||||
RECORDING_ENCODING_FRAMERATE=15
|
||||
RECORDING_ENCODING_VIDEO_BITRATE_KBPS=600
|
||||
RECORDING_ENCODING_AUDIO_BITRATE_KBPS=64
|
||||
RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0
|
||||
```
|
||||
|
||||
### Caveats
|
||||
|
||||
- **Screen-share readability — think bits/frame, not bitrate**: at 720p, text legibility starts to break down below ~40 kbits/frame (= `bitrate ÷ framerate`). The recommended preset (600 kbps × 15 fps) sits at exactly that threshold, comfortable for talking heads with occasional slide sharing. The same 600 kbps at 30 fps would only deliver 20 kbits/frame and visibly blur dense slides — which is why **lowering framerate is a more screen-share-friendly lever than lowering bitrate**. For deck-heavy or IDE-share meetings, prefer the **Slide-heavy** profile (900 kbps × 15 fps ≈ 60 kbits/frame).
|
||||
- **Motion handling**: the `veryfast` x264 preset is set by LiveKit and cannot be overridden here. Low-bitrate settings will therefore show more artefacts on fast motion than an offline re-encode with a slower preset would. This is the other reason FPS reduction is the safer tuning lever for meeting recordings.
|
||||
- **Audio**: AAC at 64 kbps stereo is transparent for voice but starts to compress music noticeably. Keep 128 kbps if you expect music playback in meetings.
|
||||
- **Codec choice**: H.264 MAIN is hardcoded on purpose. Switching to HEVC or VP9 would increase egress CPU cost 2×–5×, defeating the goal of this tuning.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
# Room purge
|
||||
|
||||
Rooms pile up over time and most of them are only used once. The `purge_inactive_rooms` management command permanently deletes the rooms that have not been started for a configurable number of days. It is disabled by default.
|
||||
|
||||
## How it works
|
||||
|
||||
Each time LiveKit tells the backend that a room has started (`room_started` webhook), the backend records the date on the room (`last_started_at`).
|
||||
A room is inactive when:
|
||||
|
||||
- it was last started more than `ROOM_INACTIVITY_DELETION_DAYS` days ago, or
|
||||
- it was never started and was created more than `ROOM_INACTIVITY_DELETION_DAYS` days ago.
|
||||
|
||||
Rooms that existed before this feature was deployed are considered started on the day of the release, so none of them can be purged before a full inactivity period has elapsed.
|
||||
|
||||
The command is meant to run once a day. The Helm chart schedules it in `backend.cronjobs` (`purge-inactive-rooms`, 01:00); it does nothing until `ROOM_INACTIVITY_DELETION_DAYS` is set.
|
||||
|
||||
```bash
|
||||
python manage.py purge_inactive_rooms # delete the inactive rooms
|
||||
python manage.py purge_inactive_rooms --dry-run # only list the rooms that would be deleted
|
||||
```
|
||||
|
||||
## Rooms that are kept
|
||||
|
||||
A recording can only be reached through its room. An inactive room is kept as long as it holds a saved recording its users may still access:
|
||||
|
||||
- with `RECORDING_EXPIRATION_DAYS` set, a saved recording created less than that many days ago,
|
||||
- with `RECORDING_EXPIRATION_DAYS` unset, any saved recording.
|
||||
|
||||
## What happens to a purged room
|
||||
|
||||
The room is deleted from the database, along with its accesses, its telephony PIN code, and the recording entries it still holds — the expired ones and those that were never saved, since any other recording would have protected the room — together with their own accesses.
|
||||
|
||||
The recording **files in the bucket are left untouched**: the backend never deletes anything from the storage, it only drops the database entries pointing at it. Removing the files is the job of the bucket lifecycle policy, which should match `RECORDING_EXPIRATION_DAYS` (see the [recording documentation](recording.md)). When the two do not match, the purge leaves objects behind: they become unreachable, since serving a recording requires its database entry, but they keep costing storage.
|
||||
|
||||
⚠️ When a room is purged, all it's configuration and access rights are also deleted. Its slug becomes available again
|
||||
and can be reused when a meeting is created from that same URL.
|
||||
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=false`, only an authenticated user can navigate to a previously existing link after the room has been purged. Doing so recreates the room in the database with a fresh configuration, with that user associated with it and granted admin rights.
|
||||
|
||||
* With `ALLOW_UNREGISTERED_ROOMS=true`, any user can reopen the purged room by navigating to the same URL. In that case, the room is created dynamically and no corresponding room entry is persisted in the database.
|
||||
@@ -42,7 +42,7 @@ sequenceDiagram
|
||||
participant Backend as Backend API
|
||||
participant Summary as Summary Service
|
||||
participant Celery as Celery Workers (transcribe-queue)
|
||||
participant S3 as S3 (Object Storage)
|
||||
participant MinIO as MinIO (Object Storage)
|
||||
participant STT as WhisperX API
|
||||
participant Docs as LaSuite Docs
|
||||
|
||||
@@ -50,7 +50,7 @@ sequenceDiagram
|
||||
Note right of Backend: Payload contains 7 params: owner_id, filename, email, sub, room, recording_date, recording_time
|
||||
|
||||
Summary->>Celery: Register task (transcribe-queue)
|
||||
Celery->>S3: Fetch audio file
|
||||
Celery->>MinIO: Fetch audio file
|
||||
Celery->>STT: Transcribe audio (WhisperX)
|
||||
STT-->>Celery: Segmented transcript
|
||||
|
||||
@@ -72,15 +72,15 @@ sequenceDiagram
|
||||
| celery_result_backend | String | `"redis://redis/0"` | Celery result backend URL. |
|
||||
| celery_max_retries | Integer | `1` | Maximum number of retries for Celery tasks. |
|
||||
| transcribe_queue | String | `"transcribe-queue"` | Name of the Celery queue for transcription tasks. |
|
||||
| aws_storage_bucket_name | String | — | Name of the S3 bucket used for storing recordings. |
|
||||
| aws_s3_endpoint_url | String | — | Endpoint URL of the S3 storage. |
|
||||
| aws_s3_access_key_id | String | — | Access key for S3. |
|
||||
| aws_s3_secret_access_key | Secret | — | Secret key for S3. |
|
||||
| aws_s3_secure_access | Boolean | `True` | Use HTTPS for S3 requests. |
|
||||
| aws_s3_region_name | String | — | Region used to sign S3 requests, passed as-is to boto3. |
|
||||
| aws_storage_bucket_name | String | — | Name of the S3/MinIO bucket used for storing recordings. |
|
||||
| aws_s3_endpoint_url | String | — | Endpoint URL of the S3/MinIO storage. |
|
||||
| aws_s3_access_key_id | String | — | Access key for S3/MinIO. |
|
||||
| aws_s3_secret_access_key | Secret | — | Secret key for S3/MinIO. |
|
||||
| aws_s3_secure_access | Boolean | `True` | Use HTTPS for S3/MinIO requests. |
|
||||
| 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. |
|
||||
|
||||
@@ -14,7 +14,7 @@ All services are required to run the minimalist instance of LaSuite Meet. Click
|
||||
| **OIDC Provider** | User authentication | [Keycloak setup](../examples/compose/keycloak/README.md) |
|
||||
| **SMTP Service** | Email notifications | - |
|
||||
|
||||
> [!NOTE] Some advanced features, as Recording and transcription, require additional services (S3-compatible object storage, email). See `/features` folder for details.
|
||||
> [!NOTE] Some advanced features, as Recording and transcription, require additional services (MinIO, email). See `/features` folder for details.
|
||||
|
||||
|
||||
## Software Requirements
|
||||
|
||||
@@ -14,7 +14,7 @@ This document is a step-by-step guide that describes how to install LaSuite Meet
|
||||
|
||||
If you do not have a kubernetes test cluster, you can install everything on a local kind cluster. In this case, the simplest way is to use our script located in this repo under **bin/start-kind.sh**.
|
||||
|
||||
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
|
||||
IMPORTANT: The kind method will only deploy meet as a local instance(127.0.0.1) that can only be accessed from the device where it has been deployed.
|
||||
|
||||
To be able to use the script, you will need to install the following components:
|
||||
|
||||
@@ -244,189 +244,126 @@ 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.
|
||||
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DATA_DIR | Data directory location | /data |
|
||||
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
|
||||
| DJANGO_SECRET_KEY | Secret key used for Django security | |
|
||||
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
|
||||
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
|
||||
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_NAME | Name of the database | meet |
|
||||
| DB_USER | User used to connect to database | dinum |
|
||||
| DB_PASSWORD | Password used to connect to the database | pass |
|
||||
| DB_HOST | Hostname of the database | localhost |
|
||||
| DB_PORT | Port to connect to database | 5432 |
|
||||
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
|
||||
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
|
||||
| AWS_S3_REGION_NAME | S3 region | |
|
||||
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
|
||||
| DJANGO_LANGUAGE_CODE | Default language | en-us |
|
||||
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
|
||||
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
||||
| ROOM_CREATION_THROTTLE_RATES | Room creation throttle rate per authenticated user | 50/minute | 50/minute |
|
||||
| ROOM_CREATION_DAILY_THROTTLE_RATES | Daily room creation cap per authenticated user | 1000/day |
|
||||
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
||||
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
||||
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
|
||||
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
|
||||
| FRONTEND_ANALYTICS | Analytics information | {} |
|
||||
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
|
||||
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
|
||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
||||
| FRONTEND_DOCUMENTATION_URL | URL of the documentation opened from the room options menu. If unset, the documentation menu item is hidden | |
|
||||
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
|
||||
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_HOST | Host of the email server | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
|
||||
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
|
||||
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
|
||||
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
|
||||
| EMAIL_BRAND_NAME | Email branding name | |
|
||||
| EMAIL_SUPPORT_EMAIL | Support email address | |
|
||||
| EMAIL_LOGO_IMG | Email logo image | |
|
||||
| EMAIL_DOMAIN | Email domain | |
|
||||
| EMAIL_APP_BASE_URL | Email app base URL | |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
|
||||
| SENTRY_DSN | Sentry server DSN | |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
|
||||
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
|
||||
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
|
||||
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
|
||||
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
|
||||
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
|
||||
| OIDC_RP_SCOPES | OIDC scopes | openid email |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
|
||||
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
|
||||
| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False |
|
||||
| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 |
|
||||
| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 |
|
||||
| LOGIN_REDIRECT_URL | Login redirect URL | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
|
||||
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
|
||||
| LIVEKIT_API_KEY | LiveKit API key | |
|
||||
| LIVEKIT_API_SECRET | LiveKit API secret | |
|
||||
| LIVEKIT_API_URL | LiveKit API URL | |
|
||||
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
|
||||
| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false |
|
||||
| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false |
|
||||
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
|
||||
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
|
||||
| ROOM_INACTIVITY_DELETION_DAYS | Days without being started after which a room is purged. Unset to never purge | |
|
||||
| RECORDING_ENABLE | Record meeting option | false |
|
||||
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
|
||||
| Option | Description | default |
|
||||
|-------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DATA_DIR | Data directory location | /data |
|
||||
| DJANGO_ALLOWED_HOSTS | Hosts that are allowed | [] |
|
||||
| DJANGO_SECRET_KEY | Secret key used for Django security | |
|
||||
| DJANGO_SILENCED_SYSTEM_CHECKS | Silence Django system checks | [] |
|
||||
| DJANGO_ALLOW_UNSECURE_USER_LISTING | Allow unsecure user listing | false |
|
||||
| DB_ENGINE | Database engine used | django.db.backends.postgresql_psycopg2 |
|
||||
| DB_NAME | Name of the database | meet |
|
||||
| DB_USER | User used to connect to database | dinum |
|
||||
| DB_PASSWORD | Password used to connect to the database | pass |
|
||||
| DB_HOST | Hostname of the database | localhost |
|
||||
| DB_PORT | Port to connect to database | 5432 |
|
||||
| STORAGES_STATICFILES_BACKEND | Static file serving engine | whitenoise.storage.CompressedManifestStaticFilesStorage |
|
||||
| AWS_S3_ENDPOINT_URL | S3 host endpoint | |
|
||||
| AWS_S3_ACCESS_KEY_ID | S3 access key | |
|
||||
| AWS_S3_SECRET_ACCESS_KEY | S3 secret key | |
|
||||
| AWS_S3_REGION_NAME | S3 region | |
|
||||
| AWS_STORAGE_BUCKET_NAME | S3 bucket name | meet-media-storage |
|
||||
| DJANGO_LANGUAGE_CODE | Default language | en-us |
|
||||
| REDIS_URL | Redis endpoint | redis://redis:6379/1 |
|
||||
| SESSION_COOKIE_AGE | Session cookie expiration in seconds | 43200 (12 hours) |
|
||||
| REQUEST_ENTRY_THROTTLE_RATES | Entry request throttle rates | 150/minute |
|
||||
| CREATION_CALLBACK_THROTTLE_RATES | Creation callback throttle rates | 600/minute |
|
||||
| SPECTACULAR_SETTINGS_ENABLE_DJANGO_DEPLOY_CHECK | Enable Django deploy check | false |
|
||||
| CSRF_TRUSTED_ORIGINS | CSRF trusted origins list | [] |
|
||||
| FRONTEND_CUSTOM_CSS_URL | URL of an additional CSS file to load in the frontend app. If set, a `<link>` tag with this URL as href is added to the `<head>` of the frontend app | |
|
||||
| FRONTEND_ANALYTICS | Analytics information | {} |
|
||||
| FRONTEND_SUPPORT | Crisp frontend support configuration, also you can pass help articles, with `help_article_transcript`, `help_article_recording`, `help_article_more_tools` | {} |
|
||||
| FRONTEND_MANIFEST_LINK | Link to the "Learn more" button on the homepage | {} |
|
||||
| FRONTEND_SILENCE_LIVEKIT_DEBUG | Silence LiveKit debug logs | false |
|
||||
| FRONTEND_IS_SILENT_LOGIN_ENABLED | Enable silent login feature | true |
|
||||
| FRONTEND_FEEDBACK | Frontend feedback configuration | {} |
|
||||
| FRONTEND_USE_FRENCH_GOV_FOOTER | Show the French government footer in the homepage | false |
|
||||
| FRONTEND_USE_PROCONNECT_BUTTON | Show a "Login with ProConnect" button in the homepage instead of a "Login" button | false |
|
||||
| DJANGO_EMAIL_BACKEND | Email backend library | django.core.mail.backends.smtp.EmailBackend |
|
||||
| DJANGO_EMAIL_HOST | Host of the email server | |
|
||||
| DJANGO_EMAIL_HOST_USER | User to connect to the email server | |
|
||||
| DJANGO_EMAIL_HOST_PASSWORD | Password to connect to the email server | |
|
||||
| DJANGO_EMAIL_PORT | Port to connect to the email server | |
|
||||
| DJANGO_EMAIL_USE_TLS | Enable TLS on email connection | false |
|
||||
| DJANGO_EMAIL_USE_SSL | Enable SSL on email connection | false |
|
||||
| DJANGO_EMAIL_FROM | Email from account | from@example.com |
|
||||
| EMAIL_BRAND_NAME | Email branding name | |
|
||||
| EMAIL_SUPPORT_EMAIL | Support email address | |
|
||||
| EMAIL_LOGO_IMG | Email logo image | |
|
||||
| EMAIL_DOMAIN | Email domain | |
|
||||
| EMAIL_APP_BASE_URL | Email app base URL | |
|
||||
| DJANGO_CORS_ALLOW_ALL_ORIGINS | Allow all CORS origins | false |
|
||||
| DJANGO_CORS_ALLOWED_ORIGINS | Origins to allow (string list) | [] |
|
||||
| DJANGO_CORS_ALLOWED_ORIGIN_REGEXES | Origins to allow (regex patterns) | [] |
|
||||
| SENTRY_DSN | Sentry server DSN | |
|
||||
| DJANGO_CELERY_BROKER_URL | Celery broker host | redis://redis:6379/0 |
|
||||
| DJANGO_CELERY_BROKER_TRANSPORT_OPTIONS | Celery broker options | {} |
|
||||
| OIDC_CREATE_USER | Create OIDC user if not exists | true |
|
||||
| OIDC_VERIFY_SSL | Verify SSL for OIDC | true |
|
||||
| OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION | Fallback to email for identification | false |
|
||||
| OIDC_RP_SIGN_ALGO | Token verification algorithm used by OIDC | RS256 |
|
||||
| OIDC_RP_CLIENT_ID | OIDC client ID | meet |
|
||||
| OIDC_RP_CLIENT_SECRET | OIDC client secret | |
|
||||
| OIDC_OP_JWKS_ENDPOINT | OIDC endpoint for JWKS | |
|
||||
| OIDC_OP_AUTHORIZATION_ENDPOINT | OIDC endpoint for authorization | |
|
||||
| OIDC_OP_TOKEN_ENDPOINT | OIDC endpoint for token | |
|
||||
| OIDC_OP_USER_ENDPOINT | OIDC endpoint for user | |
|
||||
| OIDC_OP_USER_ENDPOINT_FORMAT | OIDC endpoint format (AUTO, JWT, JSON) | AUTO |
|
||||
| OIDC_OP_LOGOUT_ENDPOINT | OIDC endpoint for logout | |
|
||||
| OIDC_AUTH_REQUEST_EXTRA_PARAMS | Extra parameters for OIDC request | {} |
|
||||
| OIDC_RP_SCOPES | OIDC scopes | openid email |
|
||||
| OIDC_USE_NONCE | Use nonce for OIDC | true |
|
||||
| OIDC_REDIRECT_REQUIRE_HTTPS | Require HTTPS for OIDC | false |
|
||||
| OIDC_REDIRECT_ALLOWED_HOSTS | Allowed redirect hosts for OIDC | [] |
|
||||
| OIDC_STORE_ID_TOKEN | Store OIDC ID token | true |
|
||||
| OIDC_REDIRECT_FIELD_NAME | Redirect field for OIDC | returnTo |
|
||||
| OIDC_USERINFO_FULLNAME_FIELDS | Full name claim from OIDC token | ["given_name", "usual_name"] |
|
||||
| OIDC_USERINFO_SHORTNAME_FIELD | Short name claim from OIDC token | given_name |
|
||||
| OIDC_USERINFO_ESSENTIAL_CLAIMS | Required claims from OIDC token | [] |
|
||||
| OIDC_USE_PKCE | Enable the use of PKCE (Proof Key for Code Exchange) during the OAuth 2.0 authorization code flow. Recommended for enhanced security. | False |
|
||||
| OIDC_PKCE_CODE_CHALLENGE_METHOD | Method used to generate the PKCE code challenge. Common values include S256 and plain. Refer to the mozilla-django-oidc documentation for supported options. | S256 |
|
||||
| OIDC_PKCE_CODE_VERIFIER_SIZE | Length of the random string used as the PKCE code verifier. Must be an integer between 43 and 128, inclusive. | 64 |
|
||||
| LOGIN_REDIRECT_URL | Login redirect URL | |
|
||||
| LOGIN_REDIRECT_URL_FAILURE | Login redirect URL for failure | |
|
||||
| LOGOUT_REDIRECT_URL | URL to redirect to on logout | |
|
||||
| ALLOW_LOGOUT_GET_METHOD | Allow logout through GET method | true |
|
||||
| LIVEKIT_API_KEY | LiveKit API key | |
|
||||
| LIVEKIT_API_SECRET | LiveKit API secret | |
|
||||
| LIVEKIT_API_URL | LiveKit API URL | |
|
||||
| LIVEKIT_VERIFY_SSL | Verify SSL for LiveKit connections | true |
|
||||
| LIVEKIT_FORCE_WSS_PROTOCOL | Enables WSS protocol conversion for legacy browser compatibility (Firefox <124, Chrome <125, Edge <125) where HTTPS URLs fail in WebSocket() constructor. | false |
|
||||
| LIVEKIT_ENABLE_FIREFOX_PROXY_WORKAROUND | Firefox-only connection warmup: pre-calls WebSocket endpoint (expecting 401) to initialize cache, resolving proxy/network connectivity issues. | false |
|
||||
| RESOURCE_DEFAULT_ACCESS_LEVEL | Default resource access level for rooms | public |
|
||||
| ALLOW_UNREGISTERED_ROOMS | Allow usage of unregistered rooms | true |
|
||||
| RECORDING_ENABLE | Record meeting option | false |
|
||||
| RECORDING_OUTPUT_FOLDER | Folder to store meetings | recordings |
|
||||
| RECORDING_WORKER_CLASSES | Worker classes for recording | {"screen_recording": "core.recording.worker.services.VideoCompositeEgressService","transcript": "core.recording.worker.services.AudioCompositeEgressService"} |
|
||||
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
|
||||
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
|
||||
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | Brevo API key for marketing emails | |
|
||||
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
|
||||
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
|
||||
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
|
||||
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
|
||||
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
|
||||
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
|
||||
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
|
||||
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
|
||||
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
|
||||
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
|
||||
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
|
||||
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
|
||||
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
|
||||
| RECORDING_EVENT_PARSER_CLASS | Storage event engine for recording | core.recording.event.parsers.MinioParser |
|
||||
| RECORDING_ENABLE_STORAGE_EVENT_AUTH | Enable storage event authorization | true |
|
||||
| RECORDING_STORAGE_EVENT_ENABLE | Enable recording storage events | false |
|
||||
| RECORDING_STORAGE_EVENT_TOKEN | Recording storage event token | |
|
||||
| RECORDING_EXPIRATION_DAYS | Recording expiration in days | |
|
||||
| RECORDING_MAX_DURATION | Maximum recording duration in milliseconds. Must match LiveKit Egress configuration exactly. | |
|
||||
| SCREEN_RECORDING_BASE_URL | Screen recording base URL | |
|
||||
| SUMMARY_SERVICE_ENDPOINT | Summary service endpoint | |
|
||||
| SUMMARY_SERVICE_API_TOKEN | API token for summary service | |
|
||||
| SIGNUP_NEW_USER_TO_MARKETING_EMAIL | Signup users to marketing emails | false |
|
||||
| MARKETING_SERVICE_CLASS | Marketing service class | core.services.marketing.BrevoMarketingService |
|
||||
| BREVO_API_KEY | Brevo API key for marketing emails | |
|
||||
| BREVO_API_CONTACT_LIST_IDS | Brevo API contact list IDs | [] |
|
||||
| DJANGO_BREVO_API_CONTACT_ATTRIBUTES | Brevo contact attributes | {"VISIO_USER": true} |
|
||||
| BREVO_API_TIMEOUT | Brevo timeout in seconds | 1 |
|
||||
| LOBBY_KEY_PREFIX | Lobby key prefix | room_lobby |
|
||||
| LOBBY_WAITING_TIMEOUT | Lobby waiting timeout in seconds | 3 |
|
||||
| LOBBY_DENIED_TIMEOUT | Lobby deny timeout in seconds | 5 |
|
||||
| LOBBY_ACCEPTED_TIMEOUT | Lobby accept timeout in seconds | 21600 (6 hours) |
|
||||
| LOBBY_NOTIFICATION_TYPE | Lobby notification types | participantWaiting |
|
||||
| LOBBY_COOKIE_NAME | Lobby cookie name | lobbyParticipantId |
|
||||
| ROOM_CREATION_CALLBACK_CACHE_TIMEOUT | Room creation callback cache timeout | 600 (10 minutes) |
|
||||
| ROOM_TELEPHONY_ENABLED | Enable SIP telephony feature | false |
|
||||
| ROOM_TELEPHONY_PIN_LENGTH | Telephony PIN length | 10 |
|
||||
| ROOM_TELEPHONY_PIN_MAX_RETRIES | Telephony PIN maximum retries | 5 |
|
||||
|
||||
@@ -16,11 +16,11 @@ info:
|
||||
* `rooms:list` – List rooms accessible to the delegated user.
|
||||
* `rooms:retrieve` – Retrieve details of a specific room.
|
||||
* `rooms:create` – Create new rooms.
|
||||
* `rooms:update` – Update the access level and configuration of existing rooms.
|
||||
* `rooms:update` – **Coming soon** Update existing rooms, e.g., add attendees to a room.
|
||||
* `rooms:delete` – **Coming soon** Delete rooms generated by the application.
|
||||
|
||||
|
||||
#### Upcoming Features
|
||||
|
||||
|
||||
* **Add attendees to a room:** You will be able to update a room to include a list of attendees, allowing them to bypass the lobby system automatically.
|
||||
* **Delete application-generated rooms:** Rooms created via the application can be deleted when no longer needed.
|
||||
|
||||
@@ -50,24 +50,10 @@ paths:
|
||||
|
||||
The application must be authorized for the user's email domain.
|
||||
The returned token expires after a configured duration and must be refreshed by calling this endpoint again.
|
||||
|
||||
Request parameters may be sent either as "application/x-www-form-urlencoded"
|
||||
(as specified by RFC 6749 for OAuth 2.0 token endpoints) or as "application/json".
|
||||
operationId: generateToken
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRequest'
|
||||
examples:
|
||||
tokenRequest:
|
||||
summary: Request token for user delegation
|
||||
value:
|
||||
client_id: "550e8400-e29b-41d4-a716-446655440000"
|
||||
client_secret: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
grant_type: "client_credentials"
|
||||
scope: "user@example.com"
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TokenRequest'
|
||||
@@ -131,19 +117,6 @@ paths:
|
||||
summary: Domain not authorized
|
||||
value:
|
||||
error: "This application is not authorized for this email domain."
|
||||
'415':
|
||||
description: |
|
||||
Unsupported media type. The request body must be sent as
|
||||
"application/x-www-form-urlencoded" or "application/json".
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
unsupportedMediaType:
|
||||
summary: Unsupported request content type
|
||||
value:
|
||||
detail: 'Unsupported media type "text/plain" in request.'
|
||||
|
||||
/rooms:
|
||||
get:
|
||||
@@ -212,7 +185,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -226,6 +198,10 @@ paths:
|
||||
description: |
|
||||
Creates a new room with secure defaults for external API usage.
|
||||
|
||||
**Restrictions:**
|
||||
- Rooms are always created with `trusted` access (no public rooms via API)
|
||||
- Room access_level can be updated from the webapp interface.
|
||||
|
||||
**Defaults:**
|
||||
- Delegated user is set as owner
|
||||
- Room slug auto-generated for uniqueness
|
||||
@@ -242,17 +218,8 @@ paths:
|
||||
$ref: '#/components/schemas/RoomCreate'
|
||||
examples:
|
||||
emptyBody:
|
||||
summary: No parameters (use all defaults)
|
||||
value: { }
|
||||
withAccessLevel:
|
||||
summary: Specify access level
|
||||
value:
|
||||
access_level: "trusted"
|
||||
withConfiguration:
|
||||
summary: Provide room configuration
|
||||
value:
|
||||
configuration:
|
||||
everyone_can_mute: true
|
||||
summary: No parameters (default)
|
||||
value: {}
|
||||
responses:
|
||||
'201':
|
||||
description: Room created successfully
|
||||
@@ -302,7 +269,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: {}
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -310,67 +276,6 @@ paths:
|
||||
'404':
|
||||
$ref: '#/components/responses/RoomNotFoundError'
|
||||
|
||||
patch:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: Update a room
|
||||
description: |
|
||||
Partially updates a room. Only the delegated user's rooms where they are
|
||||
administrator or owner can be updated; any other role gets a `403`.
|
||||
|
||||
**Updatable fields:** `access_level` and `configuration`. Every other field
|
||||
(`id`, `name`, `slug`, `pin_code`) is read-only and silently ignored when sent.
|
||||
|
||||
`configuration` is replaced as a whole, it is not merged with the stored one.
|
||||
Send the complete object you want the room to end up with.
|
||||
|
||||
Full replacement (`PUT`) is not supported. Use `PATCH` instead.
|
||||
operationId: updateRoom
|
||||
security:
|
||||
- BearerAuth: [rooms:update]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Room UUID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoomUpdate'
|
||||
examples:
|
||||
accessLevelOnly:
|
||||
summary: Change the access level
|
||||
value:
|
||||
access_level: "restricted"
|
||||
configurationOnly:
|
||||
summary: Replace the room configuration
|
||||
value:
|
||||
configuration:
|
||||
everyone_can_mute: true
|
||||
responses:
|
||||
'200':
|
||||
description: Room updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Room'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequestError'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
'404':
|
||||
$ref: '#/components/responses/RoomNotFoundError'
|
||||
'405':
|
||||
description: |
|
||||
Method not allowed, `PUT` is not supported on this endpoint.
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
@@ -439,70 +344,8 @@ components:
|
||||
|
||||
RoomCreate:
|
||||
type: object
|
||||
description: |
|
||||
Optional fields for room creation. All fields have secure defaults if omitted.
|
||||
properties:
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
RoomUpdate:
|
||||
type: object
|
||||
description: |
|
||||
Fields that can be updated on an existing room. Both are optional, omitted
|
||||
fields keep their current value.
|
||||
properties:
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
RoomConfiguration:
|
||||
type: object
|
||||
description: |
|
||||
Optional room behaviour settings. Unknown fields are rejected.
|
||||
All fields are optional and default to `null` (server-side defaults apply).
|
||||
properties:
|
||||
can_publish_sources:
|
||||
type: array
|
||||
nullable: true
|
||||
description: |
|
||||
Restricts which media tracks participants are allowed to publish.
|
||||
If `null`, all sources are permitted.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- camera
|
||||
- microphone
|
||||
- screen_share
|
||||
- screen_share_audio
|
||||
example: [ "camera", "microphone" ]
|
||||
everyone_can_mute:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: |
|
||||
Whether any participant can mute others, or only the room owner/moderator.
|
||||
If `null`, the server default applies.
|
||||
example: true
|
||||
additionalProperties: false
|
||||
|
||||
RoomAccessLevel:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- trusted
|
||||
- restricted
|
||||
description: |
|
||||
Controls who can join the room without going through the lobby.
|
||||
|
||||
- `public`: Anyone with the room link can join directly, no authentication required.
|
||||
- `trusted`: Authenticated users join directly. Unauthenticated users wait in the lobby for approval.
|
||||
- `restricted`: Only participants explicitly trusted by the owner bypass the lobby. Everyone else waits for approval regardless of authentication.
|
||||
|
||||
`public` is rejected with a `400` unless the deployment explicitly enables it
|
||||
for this API. This applies both when creating a room and when updating one.
|
||||
example: "trusted"
|
||||
description: Empty object - all room properties are auto-generated
|
||||
properties: {}
|
||||
|
||||
Room:
|
||||
type: object
|
||||
@@ -519,7 +362,10 @@ components:
|
||||
description: URL-friendly room identifier (auto-generated)
|
||||
example: "aze-eere-zer"
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Room access level (always 'trusted' for API-created rooms)
|
||||
example: "trusted"
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
@@ -547,8 +393,6 @@ components:
|
||||
type: string
|
||||
description: Default country code
|
||||
example: "US"
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
OAuthError:
|
||||
type: object
|
||||
|
||||
@@ -20,7 +20,7 @@ info:
|
||||
* `lasuite_visio:rooms:list` – List rooms accessible to the delegated user.
|
||||
* `lasuite_visio:rooms:retrieve` – Retrieve details of a specific room.
|
||||
* `lasuite_visio:rooms:create` – Create new rooms.
|
||||
* `lasuite_visio:rooms:update` – Update the access level and configuration of existing rooms.
|
||||
* `lasuite_visio:rooms:update` – **Coming soon** Update existing rooms, e.g., add attendees to a room.
|
||||
* `lasuite_visio:rooms:delete` – **Coming soon** Delete rooms generated by the application.
|
||||
|
||||
#### Upcoming Features
|
||||
@@ -48,7 +48,7 @@ paths:
|
||||
summary: List rooms
|
||||
description: |
|
||||
Returns a list of rooms accessible to the authenticated user.
|
||||
Only rooms where the user has access will be returned.
|
||||
Only rooms where the delegated user has access will be returned.
|
||||
operationId: listRooms
|
||||
security:
|
||||
- BearerAuth: [rooms:list]
|
||||
@@ -108,7 +108,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: { }
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -121,9 +120,13 @@ paths:
|
||||
summary: Create a room
|
||||
description: |
|
||||
Creates a new room with secure defaults for external API usage.
|
||||
|
||||
|
||||
**Restrictions:**
|
||||
- Rooms are always created with `trusted` access (no public rooms via API)
|
||||
- Room access_level can be updated from the webapp interface.
|
||||
|
||||
**Defaults:**
|
||||
- user is set as owner
|
||||
- Delegated user is set as owner
|
||||
- Room slug auto-generated for uniqueness
|
||||
- Telephony PIN auto-generated when enabled
|
||||
- Creation tracked with application client_id for auditing
|
||||
@@ -138,17 +141,8 @@ paths:
|
||||
$ref: '#/components/schemas/RoomCreate'
|
||||
examples:
|
||||
emptyBody:
|
||||
summary: No parameters (use all defaults)
|
||||
value: { }
|
||||
withAccessLevel:
|
||||
summary: Specify access level
|
||||
value:
|
||||
access_level: "trusted"
|
||||
withConfiguration:
|
||||
summary: Provide room configuration
|
||||
value:
|
||||
configuration:
|
||||
everyone_can_mute: true
|
||||
summary: No parameters (default)
|
||||
value: {}
|
||||
responses:
|
||||
'201':
|
||||
description: Room created successfully
|
||||
@@ -198,7 +192,6 @@ paths:
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
configuration: { }
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
@@ -206,67 +199,6 @@ paths:
|
||||
'404':
|
||||
$ref: '#/components/responses/RoomNotFoundError'
|
||||
|
||||
patch:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: Update a room
|
||||
description: |
|
||||
Partially updates a room. Only rooms where the user is administrator or
|
||||
owner can be updated; any other role gets a `403`.
|
||||
|
||||
**Updatable fields:** `access_level` and `configuration`. Every other field
|
||||
(`id`, `name`, `slug`, `pin_code`) is read-only and silently ignored when sent.
|
||||
|
||||
`configuration` is replaced as a whole, it is not merged with the stored one.
|
||||
Send the complete object you want the room to end up with.
|
||||
|
||||
Full replacement (`PUT`) is not supported. Use `PATCH` instead.
|
||||
operationId: updateRoom
|
||||
security:
|
||||
- BearerAuth: [rooms:update]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Room UUID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoomUpdate'
|
||||
examples:
|
||||
accessLevelOnly:
|
||||
summary: Change the access level
|
||||
value:
|
||||
access_level: "restricted"
|
||||
configurationOnly:
|
||||
summary: Replace the room configuration
|
||||
value:
|
||||
configuration:
|
||||
everyone_can_mute: true
|
||||
responses:
|
||||
'200':
|
||||
description: Room updated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Room'
|
||||
'400':
|
||||
$ref: '#/components/responses/BadRequestError'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
'404':
|
||||
$ref: '#/components/responses/RoomNotFoundError'
|
||||
'405':
|
||||
description: |
|
||||
Method not allowed, `PUT` is not supported on this endpoint.
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
@@ -278,72 +210,65 @@ components:
|
||||
Include in requests as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
TokenRequest:
|
||||
type: object
|
||||
required:
|
||||
- client_id
|
||||
- client_secret
|
||||
- grant_type
|
||||
- scope
|
||||
properties:
|
||||
client_id:
|
||||
type: string
|
||||
description: Application client identifier
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
client_secret:
|
||||
type: string
|
||||
format: password
|
||||
writeOnly: true
|
||||
description: Application secret key
|
||||
example: "1234567890abcdefghijklmnopqrstuvwxyz"
|
||||
grant_type:
|
||||
type: string
|
||||
enum:
|
||||
- client_credentials
|
||||
description: OAuth2 grant type (must be 'client_credentials')
|
||||
example: "client_credentials"
|
||||
scope:
|
||||
type: string
|
||||
format: email
|
||||
description: |
|
||||
Email address of the user to delegate.
|
||||
The application will act on behalf of this user.
|
||||
Note: This parameter is named 'scope' to align with OAuth2 conventions,
|
||||
but accepts an email address to identify the user. This design allows
|
||||
for future extensibility.
|
||||
example: "user@example.com"
|
||||
|
||||
TokenResponse:
|
||||
type: object
|
||||
properties:
|
||||
access_token:
|
||||
type: string
|
||||
description: JWT access token
|
||||
example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJtZWV0LWFwaSIsImF1ZCI6Im1lZXQtY2xpZW50cyIsImlhdCI6MTcwOTQ5MTIwMCwiZXhwIjoxNzA5NDk0ODAwLCJjbGllbnRfaWQiOiI1NTBlODQwMC1lMjliLTQxZDQtYTcxNi00NDY2NTU0NDAwMDAiLCJzY29wZSI6InJvb21zOmxpc3Qgcm9vbXM6cmV0cmlldmUgcm9vbXM6Y3JlYXRlIiwidXNlcl9pZCI6IjdiOGQ5YzQwLTNhMmItNGVkZi04NzFjLTJmM2Q0ZTVmNmE3YiIsImRlbGVnYXRlZCI6dHJ1ZX0.signature"
|
||||
token_type:
|
||||
type: string
|
||||
description: Token type (always 'Bearer')
|
||||
example: "Bearer"
|
||||
expires_in:
|
||||
type: integer
|
||||
description: Token lifetime in seconds
|
||||
example: 3600
|
||||
scope:
|
||||
type: string
|
||||
description: Space-separated list of granted permission scopes
|
||||
example: "rooms:list rooms:retrieve rooms:create"
|
||||
|
||||
RoomCreate:
|
||||
type: object
|
||||
description: |
|
||||
Optional fields for room creation. All fields have secure defaults if omitted.
|
||||
properties:
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
RoomUpdate:
|
||||
type: object
|
||||
description: |
|
||||
Fields that can be updated on an existing room. Both are optional, omitted
|
||||
fields keep their current value.
|
||||
properties:
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
RoomConfiguration:
|
||||
type: object
|
||||
description: |
|
||||
Optional room behaviour settings. Unknown fields are rejected.
|
||||
All fields are optional and default to `null` (server-side defaults apply).
|
||||
properties:
|
||||
can_publish_sources:
|
||||
type: array
|
||||
nullable: true
|
||||
description: |
|
||||
Restricts which media tracks participants are allowed to publish.
|
||||
If `null`, all sources are permitted.
|
||||
items:
|
||||
type: string
|
||||
enum:
|
||||
- camera
|
||||
- microphone
|
||||
- screen_share
|
||||
- screen_share_audio
|
||||
example: [ "camera", "microphone" ]
|
||||
everyone_can_mute:
|
||||
type: boolean
|
||||
nullable: true
|
||||
description: |
|
||||
Whether any participant can mute others, or only the room owner/moderator.
|
||||
If `null`, the server default applies.
|
||||
example: true
|
||||
additionalProperties: false
|
||||
|
||||
RoomAccessLevel:
|
||||
type: string
|
||||
enum:
|
||||
- public
|
||||
- trusted
|
||||
- restricted
|
||||
description: |
|
||||
Controls who can join the room without going through the lobby.
|
||||
|
||||
- `public`: Anyone with the room link can join directly, no authentication required.
|
||||
- `trusted`: Authenticated users join directly. Unauthenticated users wait in the lobby for approval.
|
||||
- `restricted`: Only participants explicitly trusted by the owner bypass the lobby. Everyone else waits for approval regardless of authentication.
|
||||
|
||||
`public` is rejected with a `400` unless the deployment explicitly enables it
|
||||
for this API. This applies both when creating a room and when updating one.
|
||||
example: "trusted"
|
||||
description: Empty object - all room properties are auto-generated
|
||||
properties: {}
|
||||
|
||||
Room:
|
||||
type: object
|
||||
@@ -360,7 +285,10 @@ components:
|
||||
description: URL-friendly room identifier (auto-generated)
|
||||
example: "aze-eere-zer"
|
||||
access_level:
|
||||
$ref: '#/components/schemas/RoomAccessLevel'
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Room access level (always 'trusted' for API-created rooms)
|
||||
example: "trusted"
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
@@ -388,8 +316,6 @@ components:
|
||||
type: string
|
||||
description: Default country code
|
||||
example: "US"
|
||||
configuration:
|
||||
$ref: '#/components/schemas/RoomConfiguration'
|
||||
|
||||
OAuthError:
|
||||
type: object
|
||||
|
||||
@@ -11,14 +11,14 @@ There are two ways to customize LaSuite Meet:
|
||||
|
||||
### How to Use
|
||||
|
||||
To use this feature, simply set the `FRONTEND_CUSTOM_CSS_URL` environment variable (of the **backend** service) to the URL of your custom CSS file. For example:
|
||||
To use this feature, simply set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. For example:
|
||||
|
||||
```javascript
|
||||
FRONTEND_CUSTOM_CSS_URL=https://example.com/custom-style.css
|
||||
FRONTEND_CSS_URL=https://example.com/custom-style.css
|
||||
```
|
||||
|
||||
> [!TIP]
|
||||
> If you serve your CSS file on the same domain as LaSuite Meet, paths are supported, i.e. `FRONTEND_CUSTOM_CSS_URL=/custom/style.css` will load `https://your-domain.com/custom/style.css`.
|
||||
> If you serve your CSS file on the same domain as LaSuite Meet, paths are supported, i.e. `FRONTEND_CSS_URL=/custom/style.css` will load `https://your-domain.com/custom/style.css`.
|
||||
|
||||
Setting this variable makes the app load your CSS at runtime, adding a `<link>` to `<head>` so you can override CSS variables and customize the frontend without rebuilding.
|
||||
|
||||
@@ -34,11 +34,10 @@ Let's say you want to change the font of our application to a custom font. You c
|
||||
|
||||
:root {
|
||||
--fonts-sans: 'Roboto', ui-sans-serif, system-ui, sans-serif;
|
||||
--avatar-cap-height: 0.7;
|
||||
}
|
||||
```
|
||||
|
||||
Then, set the `FRONTEND_CUSTOM_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the default font to the one you specified.
|
||||
Then, set the `FRONTEND_CSS_URL` environment variable to the URL of your custom CSS file. Once you've done this, our application will load your custom CSS file and apply the styles, changing the default font to the one you specified.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> You can override any CSS token—semantic or palette. See [panda.config.ts](../src/frontend/panda.config.ts) for all defined semantic tokens.
|
||||
|
||||
@@ -24,10 +24,9 @@ MEET_BASE_URL="http://localhost:8072"
|
||||
# Media
|
||||
STORAGES_STATICFILES_BACKEND=django.contrib.staticfiles.storage.StaticFilesStorage
|
||||
AWS_S3_DOMAIN_REPLACE=http://localhost:9000
|
||||
AWS_S3_ENDPOINT_URL=http://garage:9000
|
||||
AWS_S3_ACCESS_KEY_ID=meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY=meet-secret-access-key
|
||||
AWS_S3_REGION_NAME=local
|
||||
AWS_S3_ENDPOINT_URL=http://minio:9000
|
||||
AWS_S3_ACCESS_KEY_ID=meet
|
||||
AWS_S3_SECRET_ACCESS_KEY=password
|
||||
MEDIA_BASE_URL=http://localhost:3000
|
||||
FILE_UPLOAD_ENABLED=True
|
||||
|
||||
@@ -58,43 +57,20 @@ 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
|
||||
|
||||
# Recording
|
||||
RECORDING_ENABLE=True
|
||||
SUMMARY_SERVICE_VERSION=2
|
||||
SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v2/async-jobs/transcribe
|
||||
RECORDING_STORAGE_EVENT_ENABLE=True
|
||||
RECORDING_STORAGE_EVENT_TOKEN=password
|
||||
SUMMARY_SERVICE_ENDPOINT=http://app-summary-dev:8000/api/v1/tasks/
|
||||
SUMMARY_SERVICE_API_TOKEN=password
|
||||
SUMMARY_SERVICE_WEBHOOK_API_TOKEN=webhook-password
|
||||
RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording
|
||||
|
||||
# Recording encoding (LiveKit Egress advanced options).
|
||||
# When RECORDING_ENCODING_ENABLED is False (default), LiveKit uses its built-in
|
||||
# H264_720P_30 preset (1280x720, 30fps, 3000 kbps). Enable and tune to reduce
|
||||
# file size and CPU load on the egress worker.
|
||||
# RECORDING_ENCODING_ENABLED=False
|
||||
# RECORDING_ENCODING_WIDTH=1280
|
||||
# RECORDING_ENCODING_HEIGHT=720
|
||||
# RECORDING_ENCODING_FRAMERATE=30
|
||||
# RECORDING_ENCODING_VIDEO_BITRATE_KBPS=3000
|
||||
# RECORDING_ENCODING_AUDIO_BITRATE_KBPS=128
|
||||
# RECORDING_ENCODING_KEY_FRAME_INTERVAL_S=4.0
|
||||
|
||||
# Telephony
|
||||
ROOM_TELEPHONY_ENABLED=True
|
||||
|
||||
# RoomKit
|
||||
# ROOMKIT_ENABLED = True
|
||||
# ROOMKIT_SERVER_TO_SERVER_API_TOKEN = ThisIsAnExampleKeyForDevPurposeOnly
|
||||
|
||||
# Metadata
|
||||
METADATA_COLLECTOR_ENABLED=True
|
||||
|
||||
# Subtitle
|
||||
ROOM_SUBTITLE_ENABLED=False
|
||||
|
||||
FRONTEND_USE_FRENCH_GOV_FOOTER=False
|
||||
FRONTEND_USE_PROCONNECT_BUTTON=False
|
||||
|
||||
@@ -103,7 +79,3 @@ 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
|
||||
|
||||
# Diagnostics
|
||||
CONNECTION_TEST_ENABLED = True
|
||||
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
# Filled with a random value by `make create-env-files`
|
||||
GARAGE_RPC_SECRET=
|
||||
@@ -1,4 +0,0 @@
|
||||
WHISPERX_BASE_URL=https://configure-your-url.com
|
||||
WHISPERX_API_KEY=<key>
|
||||
LLM_BASE_URL=https://configure-your-url.com
|
||||
LLM_API_KEY=<key>
|
||||
@@ -1,10 +0,0 @@
|
||||
LIVEKIT_URL=ws://livekit:7880
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_SECRET=secret
|
||||
|
||||
AWS_S3_ENDPOINT_URL=garage:9000
|
||||
AWS_S3_ACCESS_KEY_ID=meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY=meet-secret-access-key
|
||||
AWS_S3_REGION_NAME=local
|
||||
AWS_STORAGE_BUCKET_NAME=meet-media-storage
|
||||
AWS_S3_SECURE_ACCESS=False
|
||||
@@ -1,24 +0,0 @@
|
||||
AWS_S3_ENDPOINT_URL=garage:9000
|
||||
AWS_S3_ACCESS_KEY_ID=meet-access-key
|
||||
AWS_S3_SECRET_ACCESS_KEY=meet-secret-access-key
|
||||
AWS_S3_REGION_NAME=local
|
||||
|
||||
LIVEKIT_URL=ws://livekit:7880
|
||||
LIVEKIT_API_KEY=devkey
|
||||
LIVEKIT_API_SECRET=secret
|
||||
|
||||
STT_PROVIDER=voxtral-vllm # voxtral-vllm, kyutai, deepgram
|
||||
ENABLE_SILERO_VAD=False
|
||||
|
||||
DEEPGRAM_API_KEY=your-deepgram-api-key
|
||||
|
||||
KYUTAI_STT_BASE_URL=url
|
||||
KYUTAI_API_KEY=your-kyutai-api-key
|
||||
|
||||
VOXTRAL_VLLM_BASE_URL=wss://<host>/v1/realtime
|
||||
VOXTRAL_VLLM_MODEL=voxtral-mini-4b-realtime-2602
|
||||
VOXTRAL_VLLM_API_KEY=your-vllm-api-key
|
||||
VOXTRAL_VLLM_TARGET_STREAMING_DELAY_MS=480
|
||||
|
||||
SENTRY_DSN=
|
||||
SENTRY_ENVIRONMENT=
|
||||
@@ -1,13 +1,12 @@
|
||||
APP_NAME="meet-app-summary-dev"
|
||||
APP_API_TOKEN="password"
|
||||
|
||||
AWS_STORAGE_BUCKET_NAME="meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL="garage:9000"
|
||||
AWS_STORAGE_BUCKET_NAME="http://meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL="minio:9000"
|
||||
AWS_S3_SECURE_ACCESS=false
|
||||
|
||||
AWS_S3_ACCESS_KEY_ID="meet-access-key"
|
||||
AWS_S3_SECRET_ACCESS_KEY="meet-secret-access-key"
|
||||
AWS_S3_REGION_NAME="local"
|
||||
AWS_S3_ACCESS_KEY_ID="meet"
|
||||
AWS_S3_SECRET_ACCESS_KEY="password"
|
||||
|
||||
WHISPERX_BASE_URL="https://configure-your-url.com"
|
||||
WHISPERX_ASR_MODEL="large-v2"
|
||||
@@ -21,14 +20,5 @@ 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=
|
||||
AUTHORIZED_TENANTS='[{"id": "meet","api_key": "password","webhook_url": "https://configure-your-url.com/api/v1.0/recordings/external-process-hook/","webhook_api_key": "webhook-password","allowed_push_to_docs": true}]'
|
||||
|
||||
@@ -3,14 +3,14 @@ Gitlint extra rule to validate that the message title is of the form
|
||||
"<gitmoji>(<scope>) <subject>"
|
||||
"""
|
||||
|
||||
import json
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import urllib.request
|
||||
|
||||
import requests
|
||||
|
||||
from gitlint.rules import CommitMessageTitle, LineRule, RuleViolation
|
||||
|
||||
GITMOJIS_URL = "https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
|
||||
|
||||
|
||||
class GitmojiTitle(LineRule):
|
||||
"""
|
||||
@@ -28,9 +28,10 @@ class GitmojiTitle(LineRule):
|
||||
Download the list possible gitmojis from the project's github repository and check that
|
||||
title contains one of them.
|
||||
"""
|
||||
with urllib.request.urlopen(GITMOJIS_URL, timeout=10) as response:
|
||||
gitmojis = json.load(response)["gitmojis"]
|
||||
emojis = [re.escape(item["emoji"]) for item in gitmojis]
|
||||
gitmojis = requests.get(
|
||||
"https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json"
|
||||
).json()["gitmojis"]
|
||||
emojis = [item["emoji"] for item in gitmojis]
|
||||
pattern = r"^({:s})\(.*\)\s[a-z].*$".format("|".join(emojis))
|
||||
if not re.search(pattern, title):
|
||||
violation_msg = 'Title does not match regex "<gitmoji>(<scope>) <subject>"'
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
publiccodeYmlVersion: 0.5.0
|
||||
name: LaSuite Meet
|
||||
applicationSuite: LaSuite
|
||||
url: https://github.com/suitenumerique/meet
|
||||
releaseDate: 2026-07-22
|
||||
platforms:
|
||||
- web
|
||||
organisation:
|
||||
name: DINUM
|
||||
uri: https://numerique.gouv.fr
|
||||
fundedBy:
|
||||
- name: Direction interministérielle du numérique (DINUM)
|
||||
uri: https://www.numerique.gouv.fr
|
||||
developmentStatus: stable
|
||||
softwareType: standalone/web
|
||||
intendedAudience:
|
||||
countries:
|
||||
- FR
|
||||
description:
|
||||
en:
|
||||
localisedName: LaSuite Meet
|
||||
shortDescription: "Open Source video conference solution, based on LiveKit"
|
||||
longDescription: "Open Source video conference application, based on LiveKit,
|
||||
Django and React. It is the official web video conference application of
|
||||
French Ministries."
|
||||
features:
|
||||
- Optimized for stability in large meetings (+100 p.)
|
||||
- Support for multiple screen sharing streams
|
||||
- Non-persistent, secure chat
|
||||
- Meeting recording
|
||||
- Meeting transcription & Summary
|
||||
- Telephony integration
|
||||
- Secure participation with robust authentication and access control
|
||||
- Customizable frontend style
|
||||
legal:
|
||||
license: MIT
|
||||
maintenance:
|
||||
type: internal
|
||||
contacts:
|
||||
- name: "Samuel Paccoud"
|
||||
email: samuel.paccoud@numerique.gouv.fr
|
||||
affiliation: DINUM
|
||||
- name: "Antoine Lebaud"
|
||||
email: antoine.lebaud.ext@numerique.gouv.fr
|
||||
affiliation: DINUM
|
||||
localisation:
|
||||
localisationReady: true
|
||||
availableLanguages:
|
||||
- fr
|
||||
- de
|
||||
- en
|
||||
- nl
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"plugins": [
|
||||
"office-addins"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:office-addins/recommended"
|
||||
]
|
||||
}
|
||||
|
Before Width: | Height: | Size: 396 B |
|
Before Width: | Height: | Size: 678 B |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 307 B |
|
Before Width: | Height: | Size: 544 B |
|
Before Width: | Height: | Size: 927 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 353 B |
|
Before Width: | Height: | Size: 756 B |
|
Before Width: | Height: | Size: 1.7 KiB |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{
|
||||
"targets": {
|
||||
"esmodules": false
|
||||
}
|
||||
}
|
||||
],
|
||||
]
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
<?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>1.0.0.0</Version>
|
||||
<ProviderName>__APP_NAME__</ProviderName>
|
||||
<DefaultLocale>fr-FR</DefaultLocale>
|
||||
<DisplayName DefaultValue="__APP_NAME__"/>
|
||||
<Description DefaultValue="Ajoutez facilement un lien de réunion __APP_NAME__ à vos emails et événements Outlook."/>
|
||||
<IconUrl DefaultValue="https://localhost:3000/assets/icon-64.png"/>
|
||||
<HighResolutionIconUrl DefaultValue="https://localhost:3000/assets/icon-128.png"/>
|
||||
<SupportUrl DefaultValue="https://lasuite.crisp.help/fr/category/visio-15sakkg/"/>
|
||||
<AppDomains>
|
||||
<AppDomain>https://localhost:3000/</AppDomain>
|
||||
</AppDomains>
|
||||
<Hosts>
|
||||
<Host Name="Mailbox"/>
|
||||
</Hosts>
|
||||
<Requirements>
|
||||
<Sets>
|
||||
<Set Name="Mailbox" MinVersion="1.1"/>
|
||||
</Sets>
|
||||
</Requirements>
|
||||
<FormSettings>
|
||||
<Form xsi:type="ItemRead">
|
||||
<DesktopSettings>
|
||||
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
<RequestedHeight>250</RequestedHeight>
|
||||
</DesktopSettings>
|
||||
</Form>
|
||||
<Form xsi:type="ItemEdit">
|
||||
<DesktopSettings>
|
||||
<SourceLocation DefaultValue="https://localhost:3000/taskpane.html"/>
|
||||
</DesktopSettings>
|
||||
</Form>
|
||||
</FormSettings>
|
||||
<Permissions>ReadWriteItem</Permissions>
|
||||
<Rule xsi:type="RuleCollection" Mode="Or">
|
||||
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Read"/>
|
||||
<Rule xsi:type="ItemIs" ItemType="Message" FormType="Edit"/>
|
||||
<Rule xsi:type="ItemIs" ItemType="Appointment" FormType="Edit"/>
|
||||
</Rule>
|
||||
<DisableEntityHighlighting>false</DisableEntityHighlighting>
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides" xsi:type="VersionOverridesV1_0">
|
||||
<Requirements>
|
||||
<bt:Sets DefaultMinVersion="1.3">
|
||||
<bt:Set Name="Mailbox"/>
|
||||
</bt:Sets>
|
||||
</Requirements>
|
||||
<Hosts>
|
||||
<Host xsi:type="MailHost">
|
||||
<DesktopFormFactor>
|
||||
<FunctionFile resid="Commands.Url"/>
|
||||
|
||||
<!-- ─── Mail: Read ─────────────────────────────────────────── -->
|
||||
<ExtensionPoint xsi:type="MessageReadCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgReadGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="msgReadOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<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="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
<!-- ─── Mail: Compose ─────────────────────────────────────── -->
|
||||
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgComposeGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="msgComposeGenerateLinkButton">
|
||||
<Label resid="GenerateLink.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="GenerateLink.Label"/>
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<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>
|
||||
</Action>
|
||||
</Control>
|
||||
<Control xsi:type="Button" id="msgComposeOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Settings.16x16"/>
|
||||
<bt:Image size="32" resid="Settings.32x32"/>
|
||||
<bt:Image size="80" resid="Settings.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
<!-- ─── Calendar: Compose (New/Edit appointment) ──────────── -->
|
||||
<ExtensionPoint xsi:type="AppointmentOrganizerCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="apptComposeGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="apptGenerateLinkButton">
|
||||
<Label resid="GenerateLink.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="GenerateLink.Label"/>
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<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>
|
||||
</Action>
|
||||
</Control>
|
||||
|
||||
<Control xsi:type="Button" id="apptOpenSettingsButton">
|
||||
<Label resid="OpenSettings.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="OpenSettings.Label"/>
|
||||
<Description resid="OpenSettings.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Settings.16x16"/>
|
||||
<bt:Image size="32" resid="Settings.32x32"/>
|
||||
<bt:Image size="80" resid="Settings.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
</DesktopFormFactor>
|
||||
</Host>
|
||||
</Hosts>
|
||||
<Resources>
|
||||
<bt:Images>
|
||||
<bt:Image id="Settings.16x16" DefaultValue="https://localhost:3000/assets/settings-16.png"/>
|
||||
<bt:Image id="Settings.32x32" DefaultValue="https://localhost:3000/assets/settings-32.png"/>
|
||||
<bt:Image id="Settings.80x80" DefaultValue="https://localhost:3000/assets/settings-80.png"/>
|
||||
<bt:Image id="Add.16x16" DefaultValue="https://localhost:3000/assets/add-16.png"/>
|
||||
<bt:Image id="Add.32x32" DefaultValue="https://localhost:3000/assets/add-32.png"/>
|
||||
<bt:Image id="Add.80x80" DefaultValue="https://localhost:3000/assets/add-80.png"/>
|
||||
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/assets/icon-16.png"/>
|
||||
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/assets/icon-32.png"/>
|
||||
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/assets/icon-80.png"/>
|
||||
</bt:Images>
|
||||
<bt:Urls>
|
||||
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/commands.html"/>
|
||||
<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="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="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>
|
||||
|
||||
<!-- ─── V1.1 override: required for shared folder / delegate support ─── -->
|
||||
<VersionOverrides xmlns="http://schemas.microsoft.com/office/mailappversionoverrides/1.1" xsi:type="VersionOverridesV1_1">
|
||||
<Requirements>
|
||||
<bt:Sets DefaultMinVersion="1.8">
|
||||
<bt:Set Name="Mailbox"/>
|
||||
</bt:Sets>
|
||||
</Requirements>
|
||||
<Hosts>
|
||||
<Host xsi:type="MailHost">
|
||||
<DesktopFormFactor>
|
||||
<FunctionFile resid="Commands.Url"/>
|
||||
<SupportsSharedFolders>true</SupportsSharedFolders>
|
||||
|
||||
<!-- ─── Mail: Read ─────────────────────────────────────────── -->
|
||||
<ExtensionPoint xsi:type="MessageReadCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgReadGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="msgReadOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<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="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
<!-- ─── Mail: Compose ─────────────────────────────────────── -->
|
||||
<ExtensionPoint xsi:type="MessageComposeCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="msgComposeGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="msgComposeGenerateLinkButton">
|
||||
<Label resid="GenerateLink.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="GenerateLink.Label"/>
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<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>
|
||||
</Action>
|
||||
</Control>
|
||||
<Control xsi:type="Button" id="msgComposeOpenPaneButton">
|
||||
<Label resid="TaskpaneButton.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="TaskpaneButton.Label"/>
|
||||
<Description resid="TaskpaneButton.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Settings.16x16"/>
|
||||
<bt:Image size="32" resid="Settings.32x32"/>
|
||||
<bt:Image size="80" resid="Settings.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
<!-- ─── Calendar: Compose (New/Edit appointment) ──────────── -->
|
||||
<ExtensionPoint xsi:type="AppointmentOrganizerCommandSurface">
|
||||
<OfficeTab id="TabDefault">
|
||||
<Group id="apptComposeGroup">
|
||||
<Label resid="GroupLabel"/>
|
||||
<Control xsi:type="Button" id="apptGenerateLinkButton">
|
||||
<Label resid="GenerateLink.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="GenerateLink.Label"/>
|
||||
<Description resid="GenerateLink.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<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>
|
||||
</Action>
|
||||
</Control>
|
||||
|
||||
<Control xsi:type="Button" id="apptOpenSettingsButton">
|
||||
<Label resid="OpenSettings.Label"/>
|
||||
<Supertip>
|
||||
<Title resid="OpenSettings.Label"/>
|
||||
<Description resid="OpenSettings.Tooltip"/>
|
||||
</Supertip>
|
||||
<Icon>
|
||||
<bt:Image size="16" resid="Settings.16x16"/>
|
||||
<bt:Image size="32" resid="Settings.32x32"/>
|
||||
<bt:Image size="80" resid="Settings.80x80"/>
|
||||
</Icon>
|
||||
<Action xsi:type="ShowTaskpane">
|
||||
<SourceLocation resid="Taskpane.Url"/>
|
||||
</Action>
|
||||
</Control>
|
||||
|
||||
</Group>
|
||||
</OfficeTab>
|
||||
</ExtensionPoint>
|
||||
|
||||
</DesktopFormFactor>
|
||||
</Host>
|
||||
</Hosts>
|
||||
<Resources>
|
||||
<bt:Images>
|
||||
<bt:Image id="Settings.16x16" DefaultValue="https://localhost:3000/addons/outlook/assets/settings-16.png"/>
|
||||
<bt:Image id="Settings.32x32" DefaultValue="https://localhost:3000/addons/outlook/assets/settings-32.png"/>
|
||||
<bt:Image id="Settings.80x80" DefaultValue="https://localhost:3000/addons/outlook/assets/settings-80.png"/>
|
||||
<bt:Image id="Add.16x16" DefaultValue="https://localhost:3000/addons/outlook/assets/add-16.png"/>
|
||||
<bt:Image id="Add.32x32" DefaultValue="https://localhost:3000/addons/outlook/assets/add-32.png"/>
|
||||
<bt:Image id="Add.80x80" DefaultValue="https://localhost:3000/addons/outlook/assets/add-80.png"/>
|
||||
<bt:Image id="Icon.16x16" DefaultValue="https://localhost:3000/addons/outlook/assets/icon-16.png"/>
|
||||
<bt:Image id="Icon.32x32" DefaultValue="https://localhost:3000/addons/outlook/assets/icon-32.png"/>
|
||||
<bt:Image id="Icon.80x80" DefaultValue="https://localhost:3000/addons/outlook/assets/icon-80.png"/>
|
||||
</bt:Images>
|
||||
<bt:Urls>
|
||||
<bt:Url id="Commands.Url" DefaultValue="https://localhost:3000/addons/outlook/commands.html"/>
|
||||
<bt:Url id="Taskpane.Url" DefaultValue="https://localhost:3000/addons/outlook/taskpane.html"/>
|
||||
</bt:Urls>
|
||||
<bt:ShortStrings>
|
||||
<!-- Default (French) -->
|
||||
<bt:String id="GroupLabel" DefaultValue="__APP_NAME__"/>
|
||||
<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="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>
|
||||
</VersionOverrides>
|
||||
</OfficeApp>
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"name": "office-addin-taskpane-js",
|
||||
"version": "0.0.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/suitenumerique/meet.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"config": {
|
||||
"app_to_debug": "outlook",
|
||||
"app_type_to_debug": "desktop",
|
||||
"dev_server_port": 3000
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"build:dev": "webpack --mode development",
|
||||
"dev-server": "webpack serve --mode development",
|
||||
"lint": "office-addin-lint check",
|
||||
"lint:fix": "office-addin-lint fix",
|
||||
"prettier": "office-addin-lint prettier",
|
||||
"signin": "office-addin-dev-settings m365-account login",
|
||||
"signout": "office-addin-dev-settings m365-account logout",
|
||||
"start": "office-addin-debugging start manifest.xml",
|
||||
"stop": "office-addin-debugging stop manifest.xml",
|
||||
"validate": "office-addin-manifest validate manifest.xml",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": "3.50.0",
|
||||
"i18next": "26.4.2",
|
||||
"i18next-browser-languagedetector": "8.2.1",
|
||||
"regenerator-runtime": "0.14.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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",
|
||||
"browserslist": [
|
||||
"last 2 versions",
|
||||
"ie 11"
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title data-app-name></title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
@@ -1,134 +0,0 @@
|
||||
/* 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()
|
||||
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
}
|
||||
});
|
||||
|
||||
function notify(message) {
|
||||
Office.context.mailbox.item.notificationMessages.replaceAsync("meetNotif", {
|
||||
type: Office.MailboxEnums.ItemNotificationMessageType.InformationalMessage,
|
||||
message,
|
||||
persistent: false,
|
||||
icon: "Icon.16x16",
|
||||
});
|
||||
}
|
||||
|
||||
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 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.setSelectedDataAsync(text, { coercionType }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
notify(t("meeting.error.details", { message: setResult.error.message }));
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.itemType !== Office.MailboxEnums.ItemType.Appointment) {
|
||||
notify(t("meeting.link_inserted"));
|
||||
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"));
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(`Erreur : ${err.message}`);
|
||||
})
|
||||
.finally(() => {
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
function connect(event) {
|
||||
initSession()
|
||||
.then((data) => {
|
||||
const stopPolling = startPolling(data.csrf_token, {
|
||||
onSuccess: (sessionData) => {
|
||||
saveSession(sessionData).then(() => {
|
||||
insertMeetingLink(event, sessionData);
|
||||
});
|
||||
},
|
||||
onTimeout: () => {
|
||||
notify(t("meeting.error.auth"));
|
||||
event.completed();
|
||||
},
|
||||
onError: (err) => {
|
||||
notify(t("meeting.error.retry"));
|
||||
event.completed();
|
||||
},
|
||||
});
|
||||
openTransitDialog(data.transit_token, {
|
||||
onCancel: () => {
|
||||
stopPolling();
|
||||
event.completed();
|
||||
},
|
||||
onError: (err) => {
|
||||
stopPolling();
|
||||
event.completed();
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
notify(t("meeting.error.details", { message: err.message }));
|
||||
event.completed();
|
||||
});
|
||||
}
|
||||
|
||||
function generateMeetingLink(event) {
|
||||
const session = loadSession();
|
||||
if (session?.access_token) {
|
||||
insertMeetingLink(event, session);
|
||||
} else {
|
||||
connect(event);
|
||||
}
|
||||
}
|
||||
|
||||
Office.actions.associate("generateMeetingLinkFromCalendar", generateMeetingLink);
|
||||
Office.actions.associate("generateMeetingLinkFromMail", generateMeetingLink);
|
||||
@@ -1,82 +0,0 @@
|
||||
const { URLS } = require("./urls");
|
||||
|
||||
function getCsrfToken() {
|
||||
return document.cookie
|
||||
.split(";")
|
||||
.filter((cookie) => cookie.trim().startsWith("csrftoken="))
|
||||
.map((cookie) => cookie.split("=")[1])
|
||||
.pop();
|
||||
}
|
||||
|
||||
function authHeaders(session) {
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds headers for CSRF-protected requests.
|
||||
*
|
||||
* Two CSRF flows coexist in this addon:
|
||||
*
|
||||
* 1. Cookie-based (Django default): used by `exchange`, called from the
|
||||
* OAuth success page in a normal browser context. Django's CSRF
|
||||
* middleware has already set the `csrftoken` cookie via the auth
|
||||
* redirect, so we read it from `document.cookie` and echo it back
|
||||
* as `X-CSRFToken`. The middleware verifies the header matches the
|
||||
* cookie. No `csrfToken` argument needed — `getCsrfToken()` handles it.
|
||||
*
|
||||
* 2. Body-passed token: used by `poll`, called from the Office dialog /
|
||||
* taskpane iframe. Cookie access inside Office iframes is unreliable
|
||||
* across Outlook clients, so we can't depend on `document.cookie`
|
||||
* being populated. Instead, `init` returns the CSRF token in its JSON
|
||||
* response body, and callers pass it explicitly to subsequent calls.
|
||||
* The token still travels as `X-CSRFToken` — only its source differs.
|
||||
*
|
||||
* The `csrfToken` parameter takes precedence when provided; falls back
|
||||
* to the cookie when omitted.
|
||||
*/
|
||||
function csrfHeaders(csrfToken) {
|
||||
const token = csrfToken || getCsrfToken();
|
||||
return {
|
||||
"Content-Type": "application/json",
|
||||
...(token && { "X-CSRFToken": token }),
|
||||
};
|
||||
}
|
||||
|
||||
async function request(path, { session, csrf, csrfToken, ...opts } = {}) {
|
||||
const headers = {
|
||||
...(session && authHeaders(session)),
|
||||
...(csrf && csrfHeaders(csrfToken)),
|
||||
...opts.headers,
|
||||
};
|
||||
const res = await fetch(path, {
|
||||
...opts,
|
||||
headers,
|
||||
credentials: csrf ? "include" : opts.credentials,
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initSession: () => request(URLS.init, { method: "POST" }),
|
||||
pollSession: (csrfToken) =>
|
||||
request(URLS.poll, {
|
||||
method: "POST",
|
||||
csrf: true,
|
||||
csrfToken,
|
||||
}),
|
||||
exchangeSession: (transitToken) =>
|
||||
request(URLS.exchange, {
|
||||
method: "POST",
|
||||
csrf: true,
|
||||
body: JSON.stringify({ transit_token: transitToken }),
|
||||
}),
|
||||
createRoom: (session) =>
|
||||
request(URLS.rooms, {
|
||||
method: "POST",
|
||||
session,
|
||||
}),
|
||||
};
|
||||
@@ -1,16 +0,0 @@
|
||||
const { APP_NAME } = require("./index");
|
||||
|
||||
function isOfficeReady() {
|
||||
return typeof Office !== "undefined" && Office?.context?.roamingSettings != null;
|
||||
}
|
||||
|
||||
function applyAppName() {
|
||||
document.querySelectorAll("[data-app-name]").forEach((el) => {
|
||||
el.textContent = APP_NAME;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
isOfficeReady,
|
||||
applyAppName,
|
||||
};
|
||||
@@ -1,48 +0,0 @@
|
||||
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,11 +0,0 @@
|
||||
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
|
||||
};
|
||||
@@ -1,107 +0,0 @@
|
||||
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,79 +0,0 @@
|
||||
const { APP_NAME, ENABLE_SOURCE_TRACKING } = require("./index");
|
||||
const { t } = require("./i18n");
|
||||
|
||||
function _formatPin(pin) {
|
||||
if (!pin) return "";
|
||||
const clean = String(pin).replace(/\s+/g, "");
|
||||
if (!clean) return "";
|
||||
if (/^\d{10}$/.test(clean)) {
|
||||
return clean.replace(/(\d{3})(\d{3})(\d{4})/, "$1 $2 $3") + "#";
|
||||
}
|
||||
return clean + "#";
|
||||
}
|
||||
|
||||
// todo - support international format
|
||||
function _formatPhone(phone) {
|
||||
if (!phone) return "";
|
||||
const clean = String(phone).replace(/\s+/g, "");
|
||||
if (/^\+33\d{9}$/.test(clean)) {
|
||||
return clean.replace(/^\+33(\d)(\d{2})(\d{2})(\d{2})(\d{2})$/, "+33 $1 $2 $3 $4 $5");
|
||||
}
|
||||
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, isWeb) {
|
||||
if (!data?.url) {
|
||||
throw new Error("buildMeetingMessage: missing url in data");
|
||||
}
|
||||
|
||||
const url = _appendTrackingParams(data.url);
|
||||
const phone = _formatPhone(data.telephony?.phone_number);
|
||||
const pin = _formatPin(data.telephony?.pin_code);
|
||||
|
||||
let textLines = "";
|
||||
let phoneLines = [];
|
||||
|
||||
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 });
|
||||
|
||||
if (isWeb) {
|
||||
phoneLines = phone && pin ? [`<br><br>${phoneOnly}`, `<br>${phoneFr}`, `<br>${pinCode}`] : [];
|
||||
|
||||
textLines = [
|
||||
"<br><br>────────────────────────────────────────",
|
||||
`<br>${join}`,
|
||||
`<br><br><a href="${url}" target="_blank">${url}</a>`,
|
||||
...phoneLines,
|
||||
"<br>────────────────────────────────────────<br>",
|
||||
];
|
||||
|
||||
} 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 };
|
||||
@@ -1,47 +0,0 @@
|
||||
const { pollSession } = require("./api");
|
||||
|
||||
const POLLING_INTERVAL_MS = 1000;
|
||||
const POLLING_TIMEOUT_MS = 3 * 60 * 1000;
|
||||
const POLLING_MAX_ATTEMPTS = POLLING_TIMEOUT_MS / POLLING_INTERVAL_MS;
|
||||
|
||||
function isPollAuthenticated(sessionData) {
|
||||
return sessionData.state === "authenticated" && sessionData.access_token;
|
||||
}
|
||||
|
||||
function startPolling(csrfToken, { onSuccess, onTimeout, onError }) {
|
||||
let pollCount = 0;
|
||||
let timeoutId = null;
|
||||
let cancelled = false;
|
||||
|
||||
const poll = () => {
|
||||
if (pollCount++ >= POLLING_MAX_ATTEMPTS) {
|
||||
onTimeout?.();
|
||||
return;
|
||||
}
|
||||
|
||||
pollSession(csrfToken)
|
||||
.then((sessionData) => {
|
||||
if (cancelled) return;
|
||||
if (isPollAuthenticated(sessionData)) {
|
||||
onSuccess?.(sessionData);
|
||||
return;
|
||||
}
|
||||
timeoutId = setTimeout(poll, POLLING_INTERVAL_MS);
|
||||
})
|
||||
.catch((err) => {
|
||||
if (cancelled) return;
|
||||
onError?.(err);
|
||||
});
|
||||
};
|
||||
|
||||
poll();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
startPolling,
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
const { isOfficeReady } = require("./helpers");
|
||||
|
||||
const SESSION_KEY = "meetSession";
|
||||
|
||||
// DEV NOTE:
|
||||
// Office.context.roamingSettings persists data in the user's mailbox and
|
||||
// synchronizes it via Exchange across all Outlook clients (desktop, web, mobile)
|
||||
// where the user signs in. This means anything stored here (including tokens)
|
||||
// leaves the local device boundary and is replicated across environments.
|
||||
//
|
||||
// Microsoft guidance explicitly advises NOT storing secrets (e.g., OAuth access
|
||||
// tokens, refresh tokens, or other sensitive credentials) in roamingSettings,
|
||||
// as it is not a secure storage mechanism and lacks OS-level protections.
|
||||
//
|
||||
// That said, for the current alpha version we accept this trade-off for simplicity,
|
||||
// with the expectation that a more secure approach (e.g., in-memory tokens) will replace this.
|
||||
function saveSession(data) {
|
||||
if (!isOfficeReady()) {
|
||||
return Promise.reject(new Error("Office not ready"));
|
||||
}
|
||||
|
||||
if (!data || !data.access_token) {
|
||||
return Promise.reject(new Error("Missing access_token"));
|
||||
}
|
||||
|
||||
const expiresInSeconds = Number(data.expires_in);
|
||||
const expiresAt =
|
||||
Number.isFinite(expiresInSeconds) && expiresInSeconds > 0
|
||||
? new Date(Date.now() + expiresInSeconds * 1000).toISOString()
|
||||
: null;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
...data,
|
||||
expiresAt,
|
||||
savedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const rs = Office.context.roamingSettings;
|
||||
rs.set(SESSION_KEY, payload);
|
||||
rs.saveAsync((result) => {
|
||||
if (result.status === Office.AsyncResultStatus.Succeeded) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(result.error?.message || "saveAsync failed"));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadSession() {
|
||||
if (!isOfficeReady()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let session = null;
|
||||
try {
|
||||
const stored = Office.context.roamingSettings.get(SESSION_KEY);
|
||||
if (stored) session = JSON.parse(stored);
|
||||
} catch (e) {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!session) return null;
|
||||
|
||||
// Fail closed if expiry is missing — backend is expected to send expires_in.
|
||||
if (!session.expiresAt) {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
const expiresTs = Date.parse(session.expiresAt);
|
||||
if (!Number.isFinite(expiresTs) || Date.now() >= expiresTs) {
|
||||
clearSession();
|
||||
return null;
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
function clearSession() {
|
||||
if (!isOfficeReady()) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
try {
|
||||
const rs = Office.context.roamingSettings;
|
||||
rs.remove(SESSION_KEY);
|
||||
rs.saveAsync((result) => {
|
||||
resolve();
|
||||
});
|
||||
} catch (e) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveSession,
|
||||
loadSession,
|
||||
clearSession,
|
||||
};
|
||||
@@ -1,43 +0,0 @@
|
||||
const { URLS } = require("./urls");
|
||||
|
||||
const DIALOG_SIGNALS = {
|
||||
ready: "ready",
|
||||
done: "done",
|
||||
};
|
||||
|
||||
const DIALOG_HEIGHT = 60;
|
||||
const DIALOG_WIDTH = 50;
|
||||
|
||||
function openTransitDialog(transitToken, { onCancel, onError }) {
|
||||
Office.context.ui.displayDialogAsync(
|
||||
URLS.transitDialog,
|
||||
{ height: DIALOG_HEIGHT, width: DIALOG_WIDTH, displayInIframe: false },
|
||||
(asyncResult) => {
|
||||
if (asyncResult.status === Office.AsyncResultStatus.Failed) {
|
||||
onError?.(asyncResult.error);
|
||||
return;
|
||||
}
|
||||
|
||||
const dialog = asyncResult.value;
|
||||
|
||||
dialog.addEventHandler(Office.EventType.DialogMessageReceived, (arg) => {
|
||||
if (arg.message === DIALOG_SIGNALS.ready) {
|
||||
dialog.messageChild(transitToken);
|
||||
return;
|
||||
}
|
||||
if (arg.message === DIALOG_SIGNALS.done) {
|
||||
return;
|
||||
}
|
||||
onCancel?.();
|
||||
dialog.close();
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
openTransitDialog,
|
||||
DIALOG_SIGNALS,
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
const TRANSIT_TOKEN_KEY = "transitToken";
|
||||
|
||||
function save(token) {
|
||||
sessionStorage.setItem(TRANSIT_TOKEN_KEY, token);
|
||||
}
|
||||
|
||||
function consume() {
|
||||
try {
|
||||
const token = sessionStorage.getItem(TRANSIT_TOKEN_KEY);
|
||||
sessionStorage.removeItem(TRANSIT_TOKEN_KEY);
|
||||
return token;
|
||||
} catch (err) {
|
||||
console.error("Failed to read transit token:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { save, consume };
|
||||
@@ -1,15 +0,0 @@
|
||||
const { BASE_URL } = require("./index");
|
||||
|
||||
const ADDONS_BASE_URL = `${BASE_URL}/api/v1.0/addons/sessions`;
|
||||
|
||||
const URLS = {
|
||||
authenticate: `${BASE_URL}/api/v1.0/authenticate/`,
|
||||
successPage: `${BASE_URL}/addons/outlook/success.html`,
|
||||
transitDialog: `${BASE_URL}/addons/outlook/transit.html`,
|
||||
init: `${ADDONS_BASE_URL}/init/`,
|
||||
poll: `${ADDONS_BASE_URL}/poll/`,
|
||||
exchange: `${ADDONS_BASE_URL}/exchange/`,
|
||||
rooms: `${BASE_URL}/external-api/v1.0/rooms/`,
|
||||
};
|
||||
|
||||
module.exports = { URLS };
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
#sideload-msg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#status {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spinner-container {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.spinner-svg {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
/* Background arc (light gray ring) */
|
||||
.spinner-track {
|
||||
stroke: #E5E7EB; /* primary.100 equivalent */
|
||||
fill: none;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
}
|
||||
|
||||
/* Foreground rotating arc */
|
||||
.spinner-arc {
|
||||
stroke: #000091; /* primary.800 equivalent */
|
||||
fill: none;
|
||||
stroke-width: 3;
|
||||
stroke-linecap: round;
|
||||
/* circumference = 2 * PI * r where r = 11 -> ~69.115 */
|
||||
/* show 30% -> dashoffset = c - 0.3 * c = ~48.38 */
|
||||
stroke-dasharray: 69.115 69.115;
|
||||
stroke-dashoffset: 48.38;
|
||||
transform-origin: center;
|
||||
animation: spinner-rotate 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spinner-rotate {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hourglass fallback for reduced motion */
|
||||
.spinner-fallback {
|
||||
display: none;
|
||||
color: #000091;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.spinner-svg {
|
||||
display: none;
|
||||
}
|
||||
.spinner-fallback {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title data-app-name></title>
|
||||
<link rel="stylesheet" href="../styles/spinner.css" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div class="spinner-container"
|
||||
role="progressbar"
|
||||
data-i18n-aria="app.loading"
|
||||
>
|
||||
<svg class="spinner-svg"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Background track -->
|
||||
<circle class="spinner-track" cx="14" cy="14" r="11"
|
||||
/>
|
||||
<!-- Rotating arc -->
|
||||
<circle class="spinner-arc" cx="14" cy="14" r="11"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Fallback hourglass icon (Remix Icon RiHourglassFill SVG path) -->
|
||||
<span class="spinner-fallback" aria-hidden="true">
|
||||
<svg width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="display: block; transform: translateY(1px);"
|
||||
>
|
||||
<path d="M6 2H18V4L13 12L18 20V22H6V20L11 12L6 4V2ZM8.535 4L13 11.143L17.465 4H8.535Z"/>
|
||||
</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,35 +0,0 @@
|
||||
const { applyAppName } = require("../common/helpers");
|
||||
const { exchangeSession } = require("../common/api");
|
||||
const { consume } = require("../common/transitToken");
|
||||
const { initI18n, translateUI } = require("../common/i18n");
|
||||
|
||||
(async () => {
|
||||
await initI18n();
|
||||
|
||||
applyAppName();
|
||||
translateUI();
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
})();
|
||||
@@ -1,64 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title data-app-name></title>
|
||||
<link rel="stylesheet" href="taskpane.css" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
|
||||
<div id="app-body">
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="view-loading">
|
||||
<p class="intro-text" data-i18n="app.loading"></p>
|
||||
</div>
|
||||
|
||||
<!-- Unauthenticated -->
|
||||
<div id="view-unauth" style="display:none;">
|
||||
<p class="intro-text">
|
||||
<span data-i18n="unauth.intro"></span>
|
||||
</p>
|
||||
<hr class="divider" />
|
||||
<button class="proconnect-button" id="btn-connect">
|
||||
<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"
|
||||
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">
|
||||
<!-- 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">
|
||||
<a id="feedback-link"
|
||||
style="display:none;"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-i18n="footer.feedback"
|
||||
></a>
|
||||
<div id="footer-right">
|
||||
<span class="version-number">1.0.0</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,199 +0,0 @@
|
||||
/* 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 ────────────────────────────────────────────────────
|
||||
|
||||
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) => {
|
||||
const stopPolling = startPolling(data.csrf_token, {
|
||||
onSuccess: (sessionData) => {
|
||||
saveSession(sessionData).then(() => showView("auth"));
|
||||
},
|
||||
onTimeout: () => {
|
||||
showView("unauth");
|
||||
},
|
||||
onError: (err) => {
|
||||
console.error(err);
|
||||
},
|
||||
});
|
||||
openTransitDialog(data.transit_token, {
|
||||
onCancel: () => stopPolling(),
|
||||
onError: (err) => {
|
||||
stopPolling();
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
clearSession().finally(() => showView("unauth"));
|
||||
}
|
||||
|
||||
// ── Meeting ──────────────────────────────────────────────────
|
||||
|
||||
function generateMeetingLink() {
|
||||
const session = loadSession();
|
||||
if (!session?.access_token) {
|
||||
showView("unauth");
|
||||
return;
|
||||
}
|
||||
|
||||
_setButtonLoading();
|
||||
|
||||
createRoom(session)
|
||||
.then((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.setSelectedDataAsync(text, { coercionType }, (setResult) => {
|
||||
if (setResult.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
reject(setResult.error);
|
||||
return;
|
||||
}
|
||||
if (item.itemType === Office.MailboxEnums.ItemType.Appointment) {
|
||||
item.location.setAsync(url, () => resolve());
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
_showRemoveButton();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.finally(() => {
|
||||
_setButtonIdle();
|
||||
});
|
||||
}
|
||||
|
||||
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";
|
||||
document.getElementById("app-body").style.display = "flex";
|
||||
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"); // this already calls _refreshMeetingButtonState internally
|
||||
} else {
|
||||
showView("unauth");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title data-app-name></title>
|
||||
<link rel="stylesheet" href="../styles/spinner.css" />
|
||||
<script nonce="NONCE_PLACEHOLDER" src="/addons/outlook/config.js"></script>
|
||||
<script nonce="NONCE_PLACEHOLDER" src="https://appsforoffice.microsoft.com/lib/1/hosted/office.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="sideload-msg" data-i18n="app.sideload"></div>
|
||||
<div
|
||||
class="spinner-container"
|
||||
role="progressbar"
|
||||
data-i18n-aria="app.loading"
|
||||
>
|
||||
<svg
|
||||
class="spinner-svg"
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<!-- Background track -->
|
||||
<circle class="spinner-track" cx="14" cy="14" r="11"
|
||||
/>
|
||||
<!-- Rotating arc -->
|
||||
<circle class="spinner-arc" cx="14" cy="14" r="11"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<!-- Fallback hourglass icon (Remix Icon RiHourglassFill SVG path) -->
|
||||
<span class="spinner-fallback" aria-hidden="true">
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style="display: block; transform: translateY(1px);"
|
||||
>
|
||||
<path d="M6 2H18V4L13 12L18 20V22H6V20L11 12L6 4V2ZM8.535 4L13 11.143L17.465 4H8.535Z"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
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() {
|
||||
const url = new URL(URLS.authenticate);
|
||||
url.searchParams.set("returnTo", URLS.successPage);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
Office.onReady(async function (info) {
|
||||
|
||||
await initI18n();
|
||||
translateUI();
|
||||
|
||||
if (info.host === Office.HostType.Outlook) {
|
||||
applyAppName();
|
||||
}
|
||||
|
||||
Office.context.ui.addHandlerAsync(
|
||||
Office.EventType.DialogParentMessageReceived,
|
||||
function (arg) {
|
||||
const transitToken = arg.message;
|
||||
|
||||
if (typeof transitToken !== "string" || transitToken.trim() === "") {
|
||||
console.error("Invalid transit token received from parent dialog.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Runs inside the dialog window.
|
||||
// Flow:
|
||||
// transit.html saves token → navigates to /authenticate → OAuth redirect →
|
||||
// success.html. sessionStorage survives because it's per-window-per-origin
|
||||
// and the dialog window persists across same-origin navigations.
|
||||
// Fragile: if the IdP opens the redirect in a new tab/window, this breaks
|
||||
// silently.
|
||||
// An alternative could be to pass the token via the OAuth `state` param
|
||||
// and read it back from the redirect URL.
|
||||
try {
|
||||
save(transitToken);
|
||||
Office.context.ui.messageParent(DIALOG_SIGNALS.done);
|
||||
window.location.href = getAuthenticateUrl();
|
||||
} catch (err) {
|
||||
console.error("Failed to store transit token:", err);
|
||||
}
|
||||
},
|
||||
function (result) {
|
||||
if (result.status !== Office.AsyncResultStatus.Succeeded) {
|
||||
console.error("Failed to register DialogParentMessageReceived handler.", result.error);
|
||||
return;
|
||||
}
|
||||
Office.context.ui.messageParent(DIALOG_SIGNALS.ready);
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,128 +0,0 @@
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
const devCerts = require("office-addin-dev-certs");
|
||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const htmlWebpackInjectAttributesPlugin = require("html-webpack-inject-attributes-plugin");
|
||||
|
||||
async function getHttpsOptions() {
|
||||
const httpsOptions = await devCerts.getHttpsServerOptions();
|
||||
return { ca: httpsOptions.ca, key: httpsOptions.key, cert: httpsOptions.cert };
|
||||
}
|
||||
|
||||
module.exports = async (env, options) => {
|
||||
const config = {
|
||||
devtool: "source-map",
|
||||
entry: {
|
||||
polyfill: ["core-js/stable", "regenerator-runtime/runtime"],
|
||||
taskpane: ["./src/taskpane/taskpane.js", "./src/taskpane/taskpane.html"],
|
||||
commands: "./src/commands/commands.js",
|
||||
transit: ["./src/transit/transit.js", "./src/transit/transit.html"],
|
||||
success: ["./src/success/success.js", "./src/success/success.html"],
|
||||
},
|
||||
output: {
|
||||
clean: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: [".html", ".js"],
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "babel-loader",
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: "html-loader",
|
||||
options: {
|
||||
sources: {
|
||||
urlFilter: (attribute, value) => {
|
||||
// Don't try to resolve the runtime-injected config
|
||||
if (value.includes("config.js")) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpg|jpeg|gif|ico)$/,
|
||||
type: "asset/resource",
|
||||
generator: {
|
||||
filename: "assets/[name][ext][query]",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "taskpane.html",
|
||||
template: "./src/taskpane/taskpane.html",
|
||||
chunks: ["polyfill", "taskpane"],
|
||||
scriptLoading: "defer",
|
||||
attributes: {
|
||||
nonce: "NONCE_PLACEHOLDER",
|
||||
},
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
from: "assets/*",
|
||||
to: "assets/[name][ext][query]",
|
||||
}
|
||||
],
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "commands.html",
|
||||
template: "./src/commands/commands.html",
|
||||
chunks: ["polyfill", "commands"],
|
||||
scriptLoading: "defer",
|
||||
attributes: {
|
||||
nonce: "NONCE_PLACEHOLDER",
|
||||
},
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "transit.html",
|
||||
template: "./src/transit/transit.html",
|
||||
chunks: ["polyfill", "transit"],
|
||||
scriptLoading: "defer",
|
||||
attributes: {
|
||||
nonce: "NONCE_PLACEHOLDER",
|
||||
},
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "success.html",
|
||||
template: "./src/success/success.html",
|
||||
chunks: ["polyfill", "success"],
|
||||
scriptLoading: "defer",
|
||||
attributes: {
|
||||
nonce: "NONCE_PLACEHOLDER",
|
||||
},
|
||||
}),
|
||||
new htmlWebpackInjectAttributesPlugin(),
|
||||
],
|
||||
devServer: {
|
||||
headers: {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
},
|
||||
server: {
|
||||
type: "https",
|
||||
options:
|
||||
env.WEBPACK_BUILD || options.https !== undefined
|
||||
? options.https
|
||||
: await getHttpsOptions(),
|
||||
},
|
||||
port: process.env.npm_package_config_dev_server_port || 3000,
|
||||
},
|
||||
};
|
||||
|
||||
return config;
|
||||
};
|
||||
@@ -1,38 +0,0 @@
|
||||
# Python
|
||||
__pycache__
|
||||
*.pyc
|
||||
**/__pycache__
|
||||
**/*.pyc
|
||||
venv
|
||||
**/.venv
|
||||
|
||||
# System-specific files
|
||||
.DS_Store
|
||||
**/.DS_Store
|
||||
|
||||
# Docker
|
||||
compose.*
|
||||
env.d
|
||||
|
||||
# Docs
|
||||
docs
|
||||
*.md
|
||||
*.log
|
||||
|
||||
# Development/test cache & configurations
|
||||
data
|
||||
.cache
|
||||
.circleci
|
||||
.git
|
||||
.iml
|
||||
db.sqlite3
|
||||
.pylint.d
|
||||
|
||||
**/.idea
|
||||
**/.vscode
|
||||
**/.pytest_cache
|
||||
**/.mypy_cache
|
||||
**/.ruff_cache
|
||||
|
||||
# Env
|
||||
.env
|
||||
@@ -1,74 +1,33 @@
|
||||
FROM python:3.14.6-slim AS base
|
||||
FROM python:3.13-slim AS base
|
||||
|
||||
# Install system dependencies required by LiveKit, fetching packages over HTTPS only for Bureautix proxy
|
||||
RUN sed -i "s|^URIs: http://|URIs: https://|" /etc/apt/sources.list.d/debian.sources \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
# Install system dependencies required by LiveKit
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libglib2.0-0 \
|
||||
libgobject-2.0-0 \
|
||||
libssl3t64 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
# ---- Builder image ----
|
||||
FROM base AS builder
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
WORKDIR /builder
|
||||
|
||||
# Install uv
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
|
||||
COPY pyproject.toml .
|
||||
|
||||
WORKDIR /app
|
||||
RUN mkdir /install && \
|
||||
pip install --prefix=/install .
|
||||
|
||||
# Install production dependencies without the project itself (cacheable layer)
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
--mount=type=bind,source=uv.lock,target=uv.lock \
|
||||
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \
|
||||
uv sync --locked --no-install-project --no-dev
|
||||
|
||||
# Install the project
|
||||
COPY . /app
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --no-dev
|
||||
|
||||
|
||||
# ---- Development image ----
|
||||
FROM base AS development
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PYTHON_DOWNLOADS=0
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.10.9 /uv /uvx /bin/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY . /app
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --locked --all-extras
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
CMD ["python", "multi_user_transcriber.py", "dev"]
|
||||
|
||||
|
||||
# ---- Production image ----
|
||||
FROM base AS production
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the pre-built virtualenv and application source
|
||||
COPY --from=builder /app /app
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
|
||||
# Remove pip to reduce attack surface in production
|
||||
RUN pip uninstall -y pip
|
||||
|
||||
# Un-privileged user running the application
|
||||
ARG DOCKER_USER
|
||||
USER ${DOCKER_USER}
|
||||
|
||||
CMD ["python", "multi_user_transcriber.py", "start"]
|
||||
# Un-privileged user running the application
|
||||
COPY --from=builder /install /usr/local
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "multi-user-transcriber.py", "start"]
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Storage parsers specific exceptions."""
|
||||
|
||||
|
||||
class MissingConfigError(Exception):
|
||||
"""Raised when a variable is not set in configuration."""
|
||||
@@ -1,413 +0,0 @@
|
||||
"""Metadata agent that extracts metadata from active room."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from dataclasses import asdict, dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
|
||||
import boto3
|
||||
from botocore.config import Config
|
||||
from botocore.exceptions import BotoCoreError, ClientError
|
||||
from dotenv import load_dotenv
|
||||
from livekit import api, rtc
|
||||
from livekit.agents import (
|
||||
Agent,
|
||||
AgentServer,
|
||||
AgentSession,
|
||||
AutoSubscribe,
|
||||
JobContext,
|
||||
JobProcess,
|
||||
JobRequest,
|
||||
RoomIO,
|
||||
WorkerPermissions,
|
||||
cli,
|
||||
utils,
|
||||
)
|
||||
from livekit.agents import (
|
||||
room_io as lk_room_io,
|
||||
)
|
||||
from livekit.plugins import silero
|
||||
|
||||
from exceptions import MissingConfigError
|
||||
from observability import configure_sentry, set_job_context
|
||||
from tasks import done_callback
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger("metadata-collector")
|
||||
|
||||
AGENT_NAME = os.getenv("METADATA_COLLECTOR_AGENT_NAME", "metadata-collector")
|
||||
|
||||
|
||||
def prewarm(proc: JobProcess):
|
||||
"""Preload voice activity detection model."""
|
||||
configure_sentry(AGENT_NAME)
|
||||
proc.userdata["vad"] = silero.VAD.load()
|
||||
|
||||
|
||||
server = AgentServer(
|
||||
permissions=WorkerPermissions(
|
||||
can_publish=False,
|
||||
can_publish_data=False,
|
||||
can_subscribe=True,
|
||||
hidden=True,
|
||||
),
|
||||
)
|
||||
server.setup_fnc = prewarm
|
||||
|
||||
|
||||
def create_s3_client():
|
||||
"""Create an S3 client for the configured endpoint and region.
|
||||
|
||||
The endpoint may be given with or without a scheme: the scheme always
|
||||
follows AWS_S3_SECURE_ACCESS.
|
||||
"""
|
||||
endpoint = (
|
||||
os.getenv("AWS_S3_ENDPOINT_URL", "")
|
||||
.removeprefix("https://")
|
||||
.removeprefix("http://")
|
||||
.rstrip("/")
|
||||
)
|
||||
secure = os.getenv("AWS_S3_SECURE_ACCESS", "False").lower() == "true"
|
||||
|
||||
return boto3.client(
|
||||
"s3",
|
||||
endpoint_url=f"{'https' if secure else 'http'}://{endpoint}",
|
||||
aws_access_key_id=os.getenv("AWS_S3_ACCESS_KEY_ID"),
|
||||
aws_secret_access_key=os.getenv("AWS_S3_SECRET_ACCESS_KEY"),
|
||||
region_name=os.getenv("AWS_S3_REGION_NAME"),
|
||||
config=Config(signature_version="s3v4", s3={"addressing_style": "path"}),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class MetadataEvent:
|
||||
"""A single timestamped event recorded during a meeting."""
|
||||
|
||||
participant_id: str
|
||||
type: str
|
||||
timestamp: datetime
|
||||
data: Optional[str] = None
|
||||
|
||||
def serialize(self) -> dict:
|
||||
"""Return a JSON-serializable dictionary representation of the event."""
|
||||
data = asdict(self)
|
||||
data["timestamp"] = self.timestamp.isoformat()
|
||||
return data
|
||||
|
||||
|
||||
class VADAgent(Agent):
|
||||
"""Agent that monitors voice activity for a specific participant."""
|
||||
|
||||
def __init__(self, participant_identity: str, events: List):
|
||||
"""Initialize with a participant identity and shared events list."""
|
||||
super().__init__(
|
||||
instructions="not-needed",
|
||||
)
|
||||
self.participant_identity = participant_identity
|
||||
self.events = events
|
||||
|
||||
async def on_enter(self) -> None:
|
||||
"""Initialize VAD monitoring for this participant."""
|
||||
|
||||
@self.session.on("user_state_changed")
|
||||
def on_user_state(event):
|
||||
timestamp = datetime.now(timezone.utc)
|
||||
|
||||
if event.new_state == "speaking":
|
||||
event = MetadataEvent(
|
||||
participant_id=self.participant_identity,
|
||||
type="speech_start",
|
||||
timestamp=timestamp,
|
||||
)
|
||||
self.events.append(event)
|
||||
|
||||
elif event.old_state == "speaking":
|
||||
event = MetadataEvent(
|
||||
participant_id=self.participant_identity,
|
||||
type="speech_end",
|
||||
timestamp=timestamp,
|
||||
)
|
||||
self.events.append(event)
|
||||
|
||||
|
||||
class MetadataCollector:
|
||||
"""Collect meeting events across all participants in a room.
|
||||
|
||||
Creates one AgentSession per participant to capture VAD events
|
||||
(speech start/end), and listens for connection, disconnection,
|
||||
and chat events. Persists all collected events as JSON to S3
|
||||
on shutdown.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: JobContext, recording_id: str):
|
||||
"""Initialize metadata agent."""
|
||||
if (bucket_name := os.getenv("AWS_STORAGE_BUCKET_NAME")) is not None:
|
||||
self.bucket_name = bucket_name
|
||||
else:
|
||||
raise MissingConfigError
|
||||
|
||||
self.s3_client = create_s3_client()
|
||||
|
||||
self.ctx = ctx
|
||||
self._sessions: dict[str, AgentSession] = {}
|
||||
self._tasks: set[asyncio.Task] = set()
|
||||
|
||||
output_folder = os.getenv("AWS_S3_OUTPUT_FOLDER", "metadata")
|
||||
self.output_filename = f"{output_folder}/{recording_id}-metadata.json"
|
||||
|
||||
# Storage for events
|
||||
self.events = []
|
||||
self.participants = {}
|
||||
|
||||
logger.info("MetadataCollector initialized")
|
||||
|
||||
def start(self):
|
||||
"""Start listening for room-level events."""
|
||||
self.ctx.room.on("participant_disconnected", self.on_participant_disconnected)
|
||||
self.ctx.room.on("participant_name_changed", self.on_participant_name_changed)
|
||||
|
||||
self.ctx.room.register_text_stream_handler("lk.chat", self.handle_chat_stream)
|
||||
|
||||
logger.info("Started listening for participant events")
|
||||
|
||||
async def on_chat_message_received(
|
||||
self, reader: rtc.TextStreamReader, participant_identity: str
|
||||
):
|
||||
"""Read a complete chat message and record it as an event."""
|
||||
full_text = await reader.read_all()
|
||||
logger.info("Received chat message from %s", participant_identity)
|
||||
|
||||
self.events.append(
|
||||
MetadataEvent(
|
||||
participant_id=participant_identity,
|
||||
type="chat_received",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
data=full_text,
|
||||
)
|
||||
)
|
||||
|
||||
def handle_chat_stream(self, reader, participant_identity):
|
||||
"""Schedule async processing of an incoming chat stream."""
|
||||
task = asyncio.create_task(
|
||||
self.on_chat_message_received(reader, participant_identity)
|
||||
)
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(
|
||||
done_callback(
|
||||
logger,
|
||||
self._tasks,
|
||||
f"process chat stream from {participant_identity}",
|
||||
)
|
||||
)
|
||||
|
||||
def save(self):
|
||||
"""Serialize collected events and upload as JSON to S3."""
|
||||
logger.info("Persisting metadata...")
|
||||
|
||||
participants = []
|
||||
for k, v in self.participants.items():
|
||||
participants.append({"participantId": k, "name": v})
|
||||
|
||||
sorted_events = sorted(self.events, key=lambda e: e.timestamp)
|
||||
|
||||
payload = {
|
||||
"events": [event.serialize() for event in sorted_events],
|
||||
"participants": participants,
|
||||
}
|
||||
|
||||
data = json.dumps(payload, indent=2).encode("utf-8")
|
||||
|
||||
try:
|
||||
self.s3_client.put_object(
|
||||
Bucket=self.bucket_name,
|
||||
Key=self.output_filename,
|
||||
Body=data,
|
||||
ContentType="application/json",
|
||||
)
|
||||
logger.info(
|
||||
"Uploaded speaker meeting metadata",
|
||||
)
|
||||
except (BotoCoreError, ClientError):
|
||||
logger.exception(
|
||||
"Failed to upload meeting metadata",
|
||||
)
|
||||
|
||||
async def aclose(self):
|
||||
"""Close all sessions and cleanup resources."""
|
||||
logger.info("Closing all VAD monitoring sessions…")
|
||||
|
||||
await utils.aio.cancel_and_wait(*self._tasks)
|
||||
|
||||
await asyncio.gather(
|
||||
*[self._close_session(session) for session in self._sessions.values()],
|
||||
return_exceptions=True,
|
||||
)
|
||||
|
||||
self.ctx.room.off("participant_disconnected", self.on_participant_disconnected)
|
||||
self.ctx.room.off("participant_name_changed", self.on_participant_name_changed)
|
||||
|
||||
logger.info("All VAD sessions closed")
|
||||
self.save()
|
||||
|
||||
async def on_participant_entrypoint(
|
||||
self, ctx: JobContext, participant: rtc.RemoteParticipant
|
||||
):
|
||||
"""Handle new participant by starting a VAD monitoring session."""
|
||||
if participant.identity in self._sessions:
|
||||
logger.debug("Session already exists for %s", participant.identity)
|
||||
return
|
||||
|
||||
self.events.append(
|
||||
MetadataEvent(
|
||||
participant_id=participant.identity,
|
||||
type="participant_connected",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
self.participants[participant.identity] = participant.name
|
||||
|
||||
logger.info("New participant connected: %s", participant.identity)
|
||||
try:
|
||||
session = await self._start_session(participant)
|
||||
self._sessions[participant.identity] = session
|
||||
except Exception:
|
||||
logger.exception("Failed to start session for %s", participant.identity)
|
||||
|
||||
def on_participant_disconnected(self, participant: rtc.RemoteParticipant):
|
||||
"""Handle participant disconnection by closing VAD monitoring."""
|
||||
self.events.append(
|
||||
MetadataEvent(
|
||||
participant_id=participant.identity,
|
||||
type="participant_disconnected",
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
)
|
||||
)
|
||||
|
||||
session = self._sessions.pop(participant.identity, None)
|
||||
if session is None:
|
||||
logger.debug("No session found for %s", participant.identity)
|
||||
return
|
||||
|
||||
logger.info("Participant disconnected: %s", participant.identity)
|
||||
task = asyncio.create_task(self._close_session(session))
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(
|
||||
done_callback(
|
||||
logger,
|
||||
self._tasks,
|
||||
f"close VAD session for {participant.identity}",
|
||||
on_success=lambda _: logger.info(
|
||||
"VAD session closed for %s (remaining sessions: %d)",
|
||||
participant.identity,
|
||||
len(self._sessions),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def on_participant_name_changed(self, participant: rtc.RemoteParticipant):
|
||||
"""Update stored participant name when it changes."""
|
||||
logger.info("Participant's name changed: %s", participant.identity)
|
||||
self.participants[participant.identity] = participant.name
|
||||
|
||||
async def _start_session(self, participant: rtc.RemoteParticipant) -> AgentSession:
|
||||
"""Create and start VAD monitoring session for participant."""
|
||||
if participant.identity in self._sessions:
|
||||
return self._sessions[participant.identity]
|
||||
|
||||
# Create session with VAD only - no STT, LLM, or TTS
|
||||
session = AgentSession(
|
||||
vad=self.ctx.proc.userdata["vad"],
|
||||
turn_detection="vad",
|
||||
user_away_timeout=30.0,
|
||||
)
|
||||
|
||||
# Set up room IO to receive audio from this specific participant
|
||||
room_io = RoomIO(
|
||||
agent_session=session,
|
||||
room=self.ctx.room,
|
||||
participant=participant,
|
||||
options=lk_room_io.RoomOptions(
|
||||
audio_input=lk_room_io.AudioInputOptions(),
|
||||
text_input=False,
|
||||
audio_output=False,
|
||||
text_output=False,
|
||||
),
|
||||
)
|
||||
|
||||
await room_io.start()
|
||||
await session.start(
|
||||
agent=VADAgent(
|
||||
participant_identity=participant.identity, events=self.events
|
||||
)
|
||||
)
|
||||
|
||||
return session
|
||||
|
||||
async def _close_session(self, session: AgentSession) -> None:
|
||||
"""Close and cleanup VAD monitoring session."""
|
||||
try:
|
||||
await session.aclose()
|
||||
except Exception:
|
||||
logger.exception("Error closing session")
|
||||
|
||||
|
||||
async def handle_job_request(job_req: JobRequest) -> None:
|
||||
"""Accept or reject the job request based on agent presence in the room."""
|
||||
room_name = job_req.room.name
|
||||
recording_id = job_req.job.metadata
|
||||
agent_identity = f"{AGENT_NAME}-{room_name}"
|
||||
|
||||
async with api.LiveKitAPI() as lk:
|
||||
try:
|
||||
resp = await lk.room.list_participants(
|
||||
list=api.ListParticipantsRequest(room=room_name)
|
||||
)
|
||||
already_present = any(
|
||||
p.kind == rtc.ParticipantKind.PARTICIPANT_KIND_AGENT
|
||||
and p.identity == agent_identity
|
||||
for p in resp.participants
|
||||
)
|
||||
if already_present:
|
||||
logger.info("Agent already in the room '%s' — reject", room_name)
|
||||
await job_req.reject()
|
||||
else:
|
||||
logger.info(
|
||||
"Accept job for '%s' — identity=%s", room_name, agent_identity
|
||||
)
|
||||
await job_req.accept(identity=agent_identity, metadata=recording_id)
|
||||
except Exception:
|
||||
logger.exception("Error treating the job for '%s'", room_name)
|
||||
await job_req.reject()
|
||||
|
||||
|
||||
@server.rtc_session(agent_name=AGENT_NAME, on_request=handle_job_request)
|
||||
async def entrypoint(ctx: JobContext):
|
||||
"""Initialize and run the metadata collector."""
|
||||
set_job_context(room=ctx.room.name, job_id=ctx.job.id)
|
||||
|
||||
logger.info("Starting metadata agent in room: %s", ctx.room.name)
|
||||
recording_id = ctx.job.metadata
|
||||
metadata_collector = MetadataCollector(ctx, recording_id)
|
||||
metadata_collector.start()
|
||||
|
||||
ctx.add_participant_entrypoint(metadata_collector.on_participant_entrypoint)
|
||||
|
||||
await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
|
||||
|
||||
async def cleanup():
|
||||
logger.info("Shutting down metadata collector...")
|
||||
await metadata_collector.aclose()
|
||||
|
||||
ctx.add_shutdown_callback(cleanup)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize Sentry for the worker process. Each job runs in its own
|
||||
# (forked) process and re-initializes Sentry via prewarm().
|
||||
configure_sentry(AGENT_NAME)
|
||||
cli.run_app(server)
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Multi user transcription agent."""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import os
|
||||
|
||||
@@ -26,10 +25,6 @@ from livekit.agents import (
|
||||
)
|
||||
from livekit.plugins import deepgram, silero
|
||||
|
||||
import voxtral_vllm_stt
|
||||
from observability import configure_sentry, set_job_context
|
||||
from tasks import done_callback
|
||||
|
||||
load_dotenv()
|
||||
|
||||
logger = logging.getLogger("transcriber")
|
||||
@@ -38,18 +33,9 @@ TRANSCRIBER_AGENT_NAME = os.getenv("TRANSCRIBER_AGENT_NAME", "multi-user-transcr
|
||||
STT_PROVIDER = os.getenv("STT_PROVIDER", "deepgram")
|
||||
ENABLE_SILERO_VAD = os.getenv("ENABLE_SILERO_VAD", "true").lower() == "true"
|
||||
|
||||
SESSION_DRAIN_TIMEOUT_S = 15.0
|
||||
|
||||
|
||||
def create_stt_provider(vad: silero.VAD | None = None):
|
||||
"""Create STT provider based on environment configuration.
|
||||
|
||||
Args:
|
||||
vad: Shared, prewarmed VAD instance. Required in practice for
|
||||
voxtral-vllm (no server-side endpointing): if omitted, the plugin
|
||||
loads its own Silero model synchronously on the event loop, once
|
||||
per participant, freezing all active sessions for the duration.
|
||||
"""
|
||||
def create_stt_provider():
|
||||
"""Create STT provider based on environment configuration."""
|
||||
if STT_PROVIDER == "deepgram":
|
||||
# Note: Not all Deepgram API parameters are supported by the LiveKit plugin
|
||||
# detect_language is NOT supported for real-time streaming
|
||||
@@ -60,9 +46,6 @@ def create_stt_provider(vad: silero.VAD | None = None):
|
||||
)
|
||||
elif STT_PROVIDER == "kyutai":
|
||||
_stt_instance = kyutai.STT(base_url=os.getenv("KYUTAI_STT_BASE_URL"))
|
||||
elif STT_PROVIDER == "voxtral-vllm":
|
||||
# The plugin resolves base_url / model / api_key from the environment.
|
||||
_stt_instance = voxtral_vllm_stt.STT(vad=vad)
|
||||
else:
|
||||
raise ValueError(f"Unknown STT_PROVIDER: {STT_PROVIDER}")
|
||||
|
||||
@@ -72,9 +55,9 @@ def create_stt_provider(vad: silero.VAD | None = None):
|
||||
class Transcriber(Agent):
|
||||
"""Create a transcription agent for a specific participant."""
|
||||
|
||||
def __init__(self, *, participant_identity: str, vad: silero.VAD | None = None):
|
||||
def __init__(self, *, participant_identity: str):
|
||||
"""Init transcription agent."""
|
||||
stt = create_stt_provider(vad=vad)
|
||||
stt = create_stt_provider()
|
||||
|
||||
super().__init__(
|
||||
instructions="not-needed",
|
||||
@@ -90,7 +73,6 @@ class MultiUserTranscriber:
|
||||
"""Init multi user transcription agent."""
|
||||
self.ctx = ctx
|
||||
self._sessions: dict[str, AgentSession] = {}
|
||||
self._starting: dict[str, asyncio.Task] = {}
|
||||
self._tasks: set[asyncio.Task] = set()
|
||||
|
||||
def start(self):
|
||||
@@ -111,51 +93,36 @@ class MultiUserTranscriber:
|
||||
|
||||
def on_participant_connected(self, participant: rtc.RemoteParticipant):
|
||||
"""Handle new participant connection by starting transcription session."""
|
||||
identity = participant.identity
|
||||
if identity in self._sessions or identity in self._starting:
|
||||
if participant.identity in self._sessions:
|
||||
return
|
||||
|
||||
logger.info(f"starting session for {identity}")
|
||||
logger.info(f"starting session for {participant.identity}")
|
||||
task = asyncio.create_task(self._start_session(participant))
|
||||
self._starting[identity] = task
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(lambda t, i=identity: self._starting.pop(i, None))
|
||||
task.add_done_callback(
|
||||
done_callback(
|
||||
logger,
|
||||
self._tasks,
|
||||
f"start transcription session for {identity}",
|
||||
)
|
||||
)
|
||||
|
||||
def on_task_done(task: asyncio.Task):
|
||||
try:
|
||||
self._sessions[participant.identity] = task.result()
|
||||
finally:
|
||||
self._tasks.discard(task)
|
||||
|
||||
task.add_done_callback(on_task_done)
|
||||
|
||||
def on_participant_disconnected(self, participant: rtc.RemoteParticipant):
|
||||
"""Handle participant disconnection by closing transcription session."""
|
||||
if (start_task := self._starting.pop(participant.identity, None)) is not None:
|
||||
logger.info(f"cancelling pending session start for {participant.identity}")
|
||||
start_task.cancel()
|
||||
return
|
||||
|
||||
if (session := self._sessions.pop(participant.identity, None)) is None:
|
||||
if (session := self._sessions.pop(participant.identity)) is None:
|
||||
return
|
||||
|
||||
logger.info(f"closing session for {participant.identity}")
|
||||
task = asyncio.create_task(self._close_session(session))
|
||||
self._tasks.add(task)
|
||||
task.add_done_callback(
|
||||
done_callback(
|
||||
logger,
|
||||
self._tasks,
|
||||
f"close transcription session for {participant.identity}",
|
||||
)
|
||||
)
|
||||
task.add_done_callback(lambda _: self._tasks.discard(task))
|
||||
|
||||
async def _start_session(self, participant: rtc.RemoteParticipant) -> AgentSession:
|
||||
"""Create and start transcription session for participant.
|
||||
"""Create and start transcription session for participant."""
|
||||
if participant.identity in self._sessions:
|
||||
return self._sessions[participant.identity]
|
||||
|
||||
Deduplication happens synchronously in on_participant_connected via
|
||||
self._starting; by the time this coroutine runs, the identity is
|
||||
already reserved.
|
||||
"""
|
||||
vad = self.ctx.proc.userdata.get("vad", None)
|
||||
session = AgentSession(vad=vad)
|
||||
room_io = RoomIO(
|
||||
@@ -166,37 +133,22 @@ class MultiUserTranscriber:
|
||||
text_input=False, audio_output=False, text_output=True
|
||||
),
|
||||
)
|
||||
try:
|
||||
await room_io.start()
|
||||
await session.start(
|
||||
agent=Transcriber(
|
||||
participant_identity=participant.identity,
|
||||
vad=vad,
|
||||
)
|
||||
await room_io.start()
|
||||
await session.start(
|
||||
agent=Transcriber(
|
||||
participant_identity=participant.identity,
|
||||
)
|
||||
except BaseException:
|
||||
with contextlib.suppress(Exception):
|
||||
await session.aclose()
|
||||
raise
|
||||
self._sessions[participant.identity] = session
|
||||
)
|
||||
return session
|
||||
|
||||
async def _close_session(self, sess: AgentSession) -> None:
|
||||
"""Close and cleanup transcription session."""
|
||||
try:
|
||||
await asyncio.wait_for(sess.drain(), timeout=SESSION_DRAIN_TIMEOUT_S)
|
||||
except (TimeoutError, asyncio.TimeoutError):
|
||||
logger.warning(
|
||||
"session drain timed out after %.0fs; forcing close",
|
||||
SESSION_DRAIN_TIMEOUT_S,
|
||||
)
|
||||
await sess.drain()
|
||||
await sess.aclose()
|
||||
|
||||
|
||||
async def entrypoint(ctx: JobContext):
|
||||
"""Initialize and run the multi-user transcriber."""
|
||||
set_job_context(room=ctx.room.name, job_id=ctx.job.id)
|
||||
|
||||
transcriber = MultiUserTranscriber(ctx)
|
||||
transcriber.start()
|
||||
|
||||
@@ -241,15 +193,11 @@ async def handle_transcriber_job_request(job_req: JobRequest) -> None:
|
||||
|
||||
def prewarm(proc: JobProcess):
|
||||
"""Preload voice activity detection model."""
|
||||
configure_sentry(TRANSCRIBER_AGENT_NAME)
|
||||
if ENABLE_SILERO_VAD:
|
||||
proc.userdata["vad"] = silero.VAD.load()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Initialize Sentry for the worker process. Each job runs in its own
|
||||
# (forked) process and re-initializes Sentry via prewarm().
|
||||
configure_sentry(TRANSCRIBER_AGENT_NAME)
|
||||
cli.run_app(
|
||||
WorkerOptions(
|
||||
entrypoint_fnc=entrypoint,
|
||||