Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 993fab7ec5 |
@@ -14,8 +14,6 @@ on:
|
||||
|
||||
env:
|
||||
DOCKER_USER: 1001:127
|
||||
DOCKER_CONTAINER_REGISTRY_HOSTNAME: docker.io
|
||||
DOCKER_CONTAINER_REGISTRY_NAMESPACE: lasuite
|
||||
|
||||
jobs:
|
||||
build-and-push-backend:
|
||||
@@ -29,7 +27,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-backend'
|
||||
images: lasuite/meet-backend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -42,7 +40,7 @@ jobs:
|
||||
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 }}'
|
||||
docker-image-name: 'docker.io/lasuite/meet-backend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -65,7 +63,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend'
|
||||
images: lasuite/meet-frontend
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -78,7 +76,7 @@ jobs:
|
||||
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 }}'
|
||||
docker-image-name: 'docker.io/lasuite/meet-frontend:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -102,7 +100,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-frontend-dinum'
|
||||
images: lasuite/meet-frontend-dinum
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -115,7 +113,7 @@ jobs:
|
||||
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 }}'
|
||||
docker-image-name: 'docker.io/lasuite/meet-frontend-dinum:${{ github.sha }}'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -139,7 +137,7 @@ jobs:
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: '${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary'
|
||||
images: lasuite/meet-summary
|
||||
-
|
||||
name: Login to DockerHub
|
||||
if: github.event_name != 'pull_request'
|
||||
@@ -147,14 +145,6 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/summary/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-summary:${{ github.sha }}'
|
||||
docker-context: './src/summary'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
@@ -186,14 +176,6 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_HUB_USER }}
|
||||
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
|
||||
-
|
||||
name: Run trivy scan
|
||||
uses: numerique-gouv/action-trivy-cache@main
|
||||
continue-on-error: true
|
||||
with:
|
||||
docker-build-args: '-f src/agents/Dockerfile --target production'
|
||||
docker-image-name: '${{ env.DOCKER_CONTAINER_REGISTRY_HOSTNAME }}/${{ env.DOCKER_CONTAINER_REGISTRY_NAMESPACE }}/meet-agents:${{ github.sha }}'
|
||||
docker-context: './src/agents'
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v6
|
||||
|
||||
@@ -34,32 +34,6 @@ jobs:
|
||||
if: always()
|
||||
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
|
||||
|
||||
check-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
if: |
|
||||
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Check that the CHANGELOG has been modified in the current branch
|
||||
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
|
||||
|
||||
lint-changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check CHANGELOG max line length
|
||||
run: |
|
||||
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
|
||||
if [ $max_line_length -ge 80 ]; then
|
||||
echo "ERROR: CHANGELOG has lines longer than 80 characters."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
build-mails:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
@@ -209,10 +183,6 @@ jobs:
|
||||
AWS_S3_ENDPOINT_URL: http://localhost:9000
|
||||
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
|
||||
OIDC_OP_URL: https://oidc.example.com
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
|
||||
+2
-122
@@ -1,3 +1,4 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
@@ -8,125 +9,4 @@ and this project adheres to
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) expose Windows app web link #976
|
||||
- ✨(frontend) support additional shortcuts to broaden accessibility
|
||||
|
||||
### Changed
|
||||
|
||||
- ✨(frontend) add clickable settings general link in idle modal #974
|
||||
|
||||
## [1.6.0] - 2026-02-10
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) monitor throttling rate failure through sentry #964
|
||||
- 🚀(paas) add PaaS deployment scripts, tested on Scalingo #957
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿️(frontend) improve spinner reduced‑motion fallback #931
|
||||
- ♿️(frontend) fix form labels and autocomplete wiring #932
|
||||
- 🥅(summary) catch file-related exceptions when handling recording #944
|
||||
- 📝(frontend) update legal terms #956
|
||||
- ⚡️(backend) enhance django admin's loading performance #954
|
||||
- 🌐(frontend) add missing DE translation for accessibility settings
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔐(backend) enforce object-level permission checks on room endpoint #959
|
||||
- 🔒️(backend) add application validation when consuming external JWT #963
|
||||
|
||||
## [1.5.0] - 2026-01-28
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿️(frontend) adjust visual-only tooltip a11y labels #910
|
||||
- ♿️(frontend) sr pin/unpin announcements with dedicated messages #898
|
||||
- ♿(frontend) adjust sr announcements for idle disconnect timer #908
|
||||
- ♿️(frontend) add global screen reader announcer#922
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔒️(frontend) fix an XSS vulnerability on the recording page #911
|
||||
|
||||
## [1.4.0] - 2026-01-25
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(frontend) add configurable redirect for unauthenticated users #904
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿️(frontend) add accessible back button in side panel #881
|
||||
- ♿️(frontend) improve participants toggle a11y label #880
|
||||
- ♿️(frontend) make carousel image decorative #871
|
||||
- ♿️(frontend) reactions are now vocalized and configurable #849
|
||||
- ♿️(frontend) improve background effect announcements #879
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🔒(backend) prevent automatic upgrade setuptools
|
||||
- ♿(frontend) improve contrast for selected options #863
|
||||
- ♿️(frontend) announce copy state in invite dialog #877
|
||||
- 📝(frontend) align close dialog label in rooms locale #878
|
||||
- 🩹(backend) use case-insensitive email matching in the external api #887
|
||||
- 🐛(frontend) ensure transcript segments are sorted by their timestamp #899
|
||||
- 🐛(frontend) scope scrollbar gutter override to video rooms #882
|
||||
|
||||
## [1.3.0] - 2026-01-13
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(summary) add dutch and german languages
|
||||
- 🔧(agents) make Silero VAD optional
|
||||
- 🚸(frontend) explain to a user they were ejected
|
||||
|
||||
### Changed
|
||||
|
||||
- 📈(frontend) track new recording's modes
|
||||
- ♿️(frontend) improve accessibility of the background and effects menu
|
||||
- ♿️(frontend) improve SR and focus for transcript and recording #810
|
||||
- 💄(frontend) adjust spacing in the recording side panels
|
||||
- 🚸(frontend) remove the default comma delimiter in humanized durations
|
||||
|
||||
### Fixed
|
||||
|
||||
- 🐛(frontend) remove unexpected F2 tooltip when clicking video screen
|
||||
- 🩹(frontend) icon font loading to avoid text/icon flickering
|
||||
|
||||
## [1.2.0] - 2026-01-05
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(agent) support Kyutai client for subtitle
|
||||
- ✨(all) support starting transcription and recording simultaneously
|
||||
- ✨(backend) persist options on a recording
|
||||
- ✨(all) support choosing the transcription language
|
||||
- ✨(summary) add a download link to the audio/video file
|
||||
- ✨(frontend) allow unprivileged users to request a recording
|
||||
|
||||
### Changed
|
||||
|
||||
- 🚸(frontend) remove the beta badge
|
||||
- ♻️(summary) extract file handling in a robust service
|
||||
- ♻️(all) manage recording state on the backend side
|
||||
|
||||
## [1.1.0] - 2025-12-22
|
||||
|
||||
### Added
|
||||
|
||||
- ✨(backend) enable user creation via email for external integrations
|
||||
- ✨(summary) add Langfuse observability for LLM API calls
|
||||
|
||||
## [1.0.1] - 2025-12-17
|
||||
|
||||
### Changed
|
||||
|
||||
- ♿(frontend) improve accessibility:
|
||||
- ♿️(frontend) hover controls, focus, SR #803
|
||||
- ♿️(frontend) change ptt keybinding from space to v #813
|
||||
- ♿(frontend) indicate external link opens in new window on feedback #816
|
||||
- ♿(frontend) fix heading level in modal to maintain semantic hierarchy #815
|
||||
- ♿️(frontend) Improve focus management when opening and closing chat #807
|
||||
- 🔧(backend) support `_FILE` for secret environment variables #566
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
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
|
||||
RUN python -m pip install --upgrade pip setuptools
|
||||
|
||||
# Upgrade system packages to install security updates
|
||||
RUN apk update && \
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
web: bin/buildpack_start.sh
|
||||
postdeploy: python manage.py migrate
|
||||
@@ -50,9 +50,6 @@ La Suite Meet is fully self-hostable and released under the MIT License, ensurin
|
||||
|
||||
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 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
|
||||
|
||||
@@ -89,7 +86,7 @@ We hope to see many more, here is an incomplete list of public La Suite Meet ins
|
||||
| [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. |
|
||||
| [meet.demo.mosacloud.eu](https://meet.demo.mosacloud.eu/) | mosa.cloud | Demo instance of mosa.cloud, a dutch company providing services around La Suite apps. |
|
||||
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-compile script"
|
||||
|
||||
# Cleanup
|
||||
rm -rf docker docs env.d gitlint
|
||||
@@ -1,50 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -o errexit # always exit on error
|
||||
set -o pipefail # don't ignore exit codes when piping output
|
||||
|
||||
echo "-----> Running post-frontend script"
|
||||
|
||||
# Move the frontend build to the nginx root and clean up
|
||||
mkdir -p build/
|
||||
mv src/frontend/dist build/frontend-out
|
||||
|
||||
ASSETS_DIR=build/frontend-out/assets
|
||||
if [ -n "$CUSTOM_LOGO_URL" ]; then
|
||||
# Ensure https
|
||||
[[ ! "$CUSTOM_LOGO_URL" =~ ^https:// ]] && echo "[custom-logo] ERROR: URL must use HTTPS" >&2 && exit 1
|
||||
|
||||
# Prevent SSRF
|
||||
HOSTNAME=$(echo "$CUSTOM_LOGO_URL" | sed -E 's|^https://([^/:]+).*|\1|')
|
||||
[[ "$HOSTNAME" =~ ^(localhost|127\.|10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.|0\.0\.0\.0|\[::1\]) ]] && echo "[custom-logo] ERROR: SSRF blocked: $HOSTNAME" >&2 && exit 1
|
||||
|
||||
LOGO_FILE="${ASSETS_DIR}/logo.svg"
|
||||
TMP_FILE=$(mktemp "${LOGO_FILE}.XXXXXX.tmp")
|
||||
|
||||
# Actual download
|
||||
echo "[custom-logo] INFO: Downloading custom logo from: $CUSTOM_LOGO_URL"
|
||||
curl -fsSL --tlsv1.2 -o "$TMP_FILE" "$CUSTOM_LOGO_URL"
|
||||
|
||||
# Validate filesize
|
||||
FILESIZE=$(stat -c%s "$TMP_FILE" 2>/dev/null || stat -f%z "$TMP_FILE")
|
||||
[[ "$FILESIZE" -eq 0 ]] && echo "[custom-logo] ERROR: empty file" >&2 && exit 1
|
||||
[[ "$FILESIZE" -gt 5242880 ]] && echo "[custom-logo] ERROR: file too large (${FILESIZE}B > 5MB)" >&2 && exit 1
|
||||
|
||||
# Validate file type
|
||||
IS_SVG=false
|
||||
|
||||
HEADER=$(head -c 100 "$TMP_FILE" | tr -d '\0' | tr '[:upper:]' '[:lower:]')
|
||||
[[ "$HEADER" =~ ^.*"<svg".*$ ]] && IS_SVG=true
|
||||
[[ "$HEADER" =~ ^.*"<?xml".*"<svg".*$ ]] && IS_SVG=true
|
||||
|
||||
[[ "$IS_SVG" == false ]] && echo "[custom-logo] ERROR: not a valid SVG file" >&2 && exit 1
|
||||
|
||||
mv -f "$TMP_FILE" "$LOGO_FILE"
|
||||
echo "[custom-logo] INFO: Custom logo downloaded successfuly"
|
||||
fi
|
||||
|
||||
mv src/backend/* ./
|
||||
mv deploy/paas/* ./
|
||||
|
||||
echo "3.13" > .python-version
|
||||
echo "." > requirements.txt
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Start the Django backend server
|
||||
gunicorn -b 0.0.0.0:8000 meet.wsgi:application --log-file - &
|
||||
|
||||
# Start the Nginx server
|
||||
bin/run &
|
||||
|
||||
# if the current shell is killed, also terminate all its children
|
||||
trap "pkill SIGTERM -P $$" SIGTERM
|
||||
|
||||
# wait for a single child to finish,
|
||||
wait -n
|
||||
# then kill all the other tasks
|
||||
pkill -P $$
|
||||
@@ -1,160 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
# Colors for output
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Function to print colored output
|
||||
print_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
|
||||
# Function to update npm package version
|
||||
update_npm_version() {
|
||||
local component=$1
|
||||
print_info "Updating $component version..."
|
||||
cd "src/$component"
|
||||
npm version "$VERSION" --no-git-tag-version
|
||||
cd -
|
||||
}
|
||||
|
||||
# Function to update Python project version in pyproject.toml
|
||||
update_python_version() {
|
||||
local component=$1
|
||||
print_info "Updating $component version..."
|
||||
cd "src/$component"
|
||||
|
||||
if [ ! -f "pyproject.toml" ]; then
|
||||
print_error "pyproject.toml not found in src/$component!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q '^version = "' pyproject.toml; then
|
||||
sed -i.bak "s/^version = \".*\"/version = \"$VERSION\"/" pyproject.toml
|
||||
rm pyproject.toml.bak
|
||||
print_info "Updated pyproject.toml version to $VERSION"
|
||||
else
|
||||
print_error "Could not find version line in pyproject.toml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd -
|
||||
}
|
||||
|
||||
# Check if we're in a git repository
|
||||
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
||||
print_error "Not a git repository. Please run this script from the root of your project."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if working directory is clean
|
||||
if ! git diff-index --quiet HEAD --; then
|
||||
print_error "Working directory is not clean. Please commit or stash your changes first."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Ask user for release version number
|
||||
echo ""
|
||||
read -p "Enter release version number (e.g., 1.2.3): " VERSION
|
||||
|
||||
# Validate version format (basic semver check)
|
||||
if ! [[ $VERSION =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
print_error "Invalid version format. Please use semantic versioning (e.g., 1.2.3)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Release version: $VERSION"
|
||||
|
||||
# Check if branch already exists
|
||||
BRANCH_NAME="release/$VERSION"
|
||||
if git show-ref --verify --quiet "refs/heads/$BRANCH_NAME"; then
|
||||
print_error "Branch $BRANCH_NAME already exists!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create and checkout new branch
|
||||
print_info "Creating branch: $BRANCH_NAME"
|
||||
git checkout -b "$BRANCH_NAME"
|
||||
|
||||
# Update frontend
|
||||
update_npm_version "frontend"
|
||||
|
||||
# Update SDK
|
||||
update_npm_version "sdk"
|
||||
|
||||
# Update mail
|
||||
update_npm_version "mail"
|
||||
|
||||
# Update backend pyproject.toml
|
||||
update_python_version "backend"
|
||||
|
||||
# Update summary pyproject.toml
|
||||
update_python_version "summary"
|
||||
|
||||
# Update agents pyproject.toml
|
||||
update_python_version "agents"
|
||||
|
||||
# Update CHANGELOG
|
||||
print_info "Updating CHANGELOG..."
|
||||
|
||||
if [ ! -f "CHANGELOG.md" ]; then
|
||||
print_error "CHANGELOG.md not found in project root!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current date in YYYY-MM-DD format
|
||||
CURRENT_DATE=$(date +%Y-%m-%d)
|
||||
|
||||
# Replace [Unreleased] with [version number] - YYYY-MM-DD
|
||||
if grep -q '\[Unreleased\]' CHANGELOG.md; then
|
||||
sed -i.bak "s/\[Unreleased\]/[$VERSION] - $CURRENT_DATE/" CHANGELOG.md
|
||||
|
||||
# Add new [Unreleased] section after the header
|
||||
# This adds it after the line containing "Semantic Versioning"
|
||||
sed -i.bak "/Semantic Versioning/a\\
|
||||
\\
|
||||
## [Unreleased]
|
||||
" CHANGELOG.md
|
||||
|
||||
rm CHANGELOG.md.bak
|
||||
print_info "Updated CHANGELOG.md"
|
||||
else
|
||||
print_warning "Could not find [Unreleased] section in CHANGELOG.md"
|
||||
fi
|
||||
|
||||
|
||||
|
||||
# Summary
|
||||
echo ""
|
||||
print_info "Release preparation complete!"
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo " - Branch created: $BRANCH_NAME"
|
||||
echo " - Version updated to: $VERSION"
|
||||
echo " - Files modified:"
|
||||
echo " - src/frontend/package.json"
|
||||
echo " - src/sdk/package.json"
|
||||
echo " - src/mail/package.json"
|
||||
echo " - src/backend/pyproject.toml"
|
||||
echo " - src/summary/pyproject.toml"
|
||||
echo " - src/agents/pyproject.toml"
|
||||
echo " - CHANGELOG.md"
|
||||
echo ""
|
||||
print_warning "Next steps:"
|
||||
echo " 1. Review the changes: git status"
|
||||
echo " 2. Commit the changes: git add . && git commit -m 'Release $VERSION'"
|
||||
echo " 3. Push the branch: git push origin $BRANCH_NAME"
|
||||
echo ""
|
||||
+1
-11
@@ -90,9 +90,6 @@ services:
|
||||
- createwebhook
|
||||
extra_hosts:
|
||||
- "127.0.0.1.nip.io:host-gateway"
|
||||
networks:
|
||||
- resource-server
|
||||
- default
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
@@ -148,9 +145,6 @@ services:
|
||||
- ./docker/files/etc/nginx/conf.d:/etc/nginx/conf.d:ro
|
||||
depends_on:
|
||||
- keycloak
|
||||
networks:
|
||||
- resource-server
|
||||
- default
|
||||
|
||||
frontend:
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
@@ -235,7 +229,7 @@ services:
|
||||
- livekit-egress
|
||||
|
||||
livekit-egress:
|
||||
image: livekit/egress:v1.11.0
|
||||
image: livekit/egress
|
||||
environment:
|
||||
EGRESS_CONFIG_FILE: ./livekit-egress.yaml
|
||||
volumes:
|
||||
@@ -304,7 +298,3 @@ services:
|
||||
watch:
|
||||
- action: rebuild
|
||||
path: ./src/summary
|
||||
|
||||
networks:
|
||||
default:
|
||||
resource-server:
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# ERB templated nginx configuration
|
||||
# see https://doc.scalingo.com/platform/deployment/buildpacks/nginx
|
||||
|
||||
upstream backend_server {
|
||||
server localhost:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen <%= ENV["PORT"] %>;
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
|
||||
root /app/build/frontend-out;
|
||||
|
||||
# Django rest framework
|
||||
location ^~ /api/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://backend_server;
|
||||
}
|
||||
|
||||
# Django admin
|
||||
location ^~ /admin/ {
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://backend_server;
|
||||
}
|
||||
|
||||
# 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;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ RUN npm run build
|
||||
# Inject PostHog sourcemap metadata into the built assets
|
||||
# This metadata is essential for correctly mapping errors to source maps in production
|
||||
RUN set -e && \
|
||||
npx @posthog/cli@0.4.8 sourcemap inject --directory ./dist/assets
|
||||
npx @posthog/cli sourcemap inject --directory ./dist/assets
|
||||
|
||||
COPY ./docker/dinum-frontend/dinum-styles.css \
|
||||
./dist/assets/
|
||||
@@ -42,12 +42,7 @@ COPY ./docker/dinum-frontend/fonts/ \
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk update && apk upgrade libssl3 \
|
||||
libcrypto3 \
|
||||
libxml2>=2.12.7-r2 \
|
||||
libxslt>=1.1.39-r2 \
|
||||
libexpat>=2.7.2-r0 \
|
||||
libpng>=1.6.53-r0
|
||||
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2 libexpat>=2.7.2-r0
|
||||
|
||||
USER nginx
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
upstream meet_backend {
|
||||
server ${BACKEND_INTERNAL_HOST}:8000 fail_timeout=0;
|
||||
}
|
||||
|
||||
upstream meet_frontend {
|
||||
server ${FRONTEND_INTERNAL_HOST}:8080 fail_timeout=0;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 8083;
|
||||
server_name localhost;
|
||||
charset utf-8;
|
||||
|
||||
# Disables server version feedback on pages and in headers
|
||||
server_tokens off;
|
||||
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
location @proxy_to_meet_backend {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://meet_backend;
|
||||
}
|
||||
|
||||
location @proxy_to_meet_frontend {
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
proxy_redirect off;
|
||||
proxy_pass http://meet_frontend;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri @proxy_to_meet_frontend;
|
||||
}
|
||||
|
||||
location /api {
|
||||
try_files $uri @proxy_to_meet_backend;
|
||||
}
|
||||
|
||||
location /admin {
|
||||
try_files $uri @proxy_to_meet_backend;
|
||||
}
|
||||
|
||||
location /static {
|
||||
try_files $uri @proxy_to_meet_backend;
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,3 @@ redis:
|
||||
address: redis:6379
|
||||
keys:
|
||||
devkey: secret
|
||||
|
||||
webhook:
|
||||
api_key: devkey
|
||||
urls:
|
||||
- http://app-dev:8000/api/v1.0/rooms/webhooks-livekit/
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
version: '3'
|
||||
|
||||
# You can add any necessary service here that will join the same docker network
|
||||
# sharing keycloak. Services added to the 'meet_resource-server' network will be
|
||||
# able to communicate with keycloak and the backend on that network.
|
||||
services:
|
||||
# busybox service is only used for testing purposes. It provides curl to test
|
||||
# connectivity to the backend and keycloak services. Replace this with your
|
||||
# relevant application services that need to communicate with keycloak.
|
||||
busybox:
|
||||
image: alpine:latest
|
||||
privileged: true
|
||||
command: sh -c "apk add --no-cache curl && sleep infinity"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
networks:
|
||||
- default
|
||||
- meet_resource-server
|
||||
|
||||
networks:
|
||||
default: {}
|
||||
meet_resource-server:
|
||||
external: true
|
||||
@@ -1,89 +0,0 @@
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 1s
|
||||
timeout: 2s
|
||||
retries: 300
|
||||
env_file:
|
||||
- env.d/postgresql
|
||||
- env.d/common
|
||||
volumes:
|
||||
- ./data/databases/backend:/var/lib/postgresql/data
|
||||
|
||||
redis:
|
||||
image: redis:5
|
||||
|
||||
backend:
|
||||
image: lasuite/meet-backend:latest
|
||||
user: ${DOCKER_USER:-1000}
|
||||
restart: always
|
||||
env_file:
|
||||
- .env
|
||||
- env.d/common
|
||||
- env.d/postgresql
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "manage.py", "check"]
|
||||
interval: 15s
|
||||
timeout: 30s
|
||||
retries: 20
|
||||
start_period: 10s
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
redis:
|
||||
condition: service_started
|
||||
livekit:
|
||||
condition: service_started
|
||||
|
||||
frontend:
|
||||
image: lasuite/meet-frontend:latest
|
||||
user: "${DOCKER_USER:-1000}"
|
||||
entrypoint:
|
||||
- /docker-entrypoint.sh
|
||||
command: ["nginx", "-g", "daemon off;"]
|
||||
env_file:
|
||||
- .env
|
||||
- env.d/common
|
||||
# Uncomment and set your values if using our nginx proxy example
|
||||
# environment:
|
||||
# - VIRTUAL_HOST=${MEET_HOST} # used by nginx proxy
|
||||
# - VIRTUAL_PORT=8083 # used by nginx proxy
|
||||
# - LETSENCRYPT_HOST=${MEET_HOST} # used by lets encrypt to generate TLS certificate
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- ./default.conf.template:/etc/nginx/templates/docs.conf.template
|
||||
# Uncomment if using our nginx proxy example
|
||||
# networks:
|
||||
# - proxy-tier
|
||||
# - default
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
command: --config /config.yaml
|
||||
ports:
|
||||
- 7881:7881/tcp
|
||||
- 7882:7882/udp
|
||||
volumes:
|
||||
- ./livekit-server.yaml:/config.yaml
|
||||
# Uncomment and set your values if using our nginx proxy example
|
||||
# environment:
|
||||
# - VIRTUAL_HOST=${LIVEKIT_HOST} # used by nginx proxy
|
||||
# - VIRTUAL_PORT=7880 # used by nginx proxy
|
||||
# - LETSENCRYPT_HOST=${LIVEKIT_HOST} # used by lets encrypt to generate TLS certificate
|
||||
depends_on:
|
||||
redis:
|
||||
condition: service_started
|
||||
# Uncomment if using our nginx proxy example
|
||||
# networks:
|
||||
# - proxy-tier
|
||||
# - default
|
||||
|
||||
# Uncomment if using our nginx proxy example
|
||||
#networks:
|
||||
# proxy-tier:
|
||||
# external: true
|
||||
@@ -1,92 +0,0 @@
|
||||
# Deploy and Configure Keycloak for Meet
|
||||
|
||||
## Installation
|
||||
|
||||
> [!CAUTION]
|
||||
> We provide those instructions as an example, for production environments, you should follow the [official documentation](https://www.keycloak.org/documentation).
|
||||
|
||||
### Step 1: Prepare your working environment:
|
||||
|
||||
```bash
|
||||
mkdir -p keycloak/env.d && cd keycloak
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/docs/examples/compose/keycloak/compose.yaml
|
||||
curl -o env.d/kc_postgresql https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/env.d/production.dist/kc_postgresql
|
||||
curl -o env.d/keycloak https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/env.d/production.dist/keycloak
|
||||
```
|
||||
|
||||
### Step 2:. Update `env.d/` files
|
||||
|
||||
The following variables need to be updated with your own values, others can be left as is:
|
||||
|
||||
```env
|
||||
POSTGRES_PASSWORD=<generate postgres password>
|
||||
KC_HOSTNAME=https://id.yourdomain.tld # Change with your own URL
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD=<generate your password>
|
||||
```
|
||||
|
||||
### Step 3: Expose keycloak instance on https
|
||||
|
||||
> [!NOTE]
|
||||
> You can skip this section if you already have your own setup.
|
||||
|
||||
To access your Keycloak instance on the public network, it needs to be exposed on a domain with SSL termination. You can use our [example with nginx proxy and Let's Encrypt companion](../nginx-proxy/README.md) for automated creation/renewal of certificates using [acme.sh](http://acme.sh).
|
||||
|
||||
If following our example, uncomment the environment and network sections in compose file and update it with your values.
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
keycloak:
|
||||
...
|
||||
# Uncomment and set your values if using our nginx proxy example
|
||||
# environment:
|
||||
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
|
||||
# - VIRTUAL_PORT=8080 # used by nginx proxy
|
||||
# - LETSENCRYPT_HOST=id.yourdomain.tld # used by lets encrypt to generate TLS certificate
|
||||
...
|
||||
# Uncomment if using our nginx proxy example
|
||||
# networks:
|
||||
# - proxy-tier
|
||||
# - default
|
||||
|
||||
# Uncomment if using our nginx proxy example
|
||||
#networks:
|
||||
# proxy-tier:
|
||||
# external: true
|
||||
```
|
||||
|
||||
### Step 4: Start the service
|
||||
|
||||
```bash
|
||||
`docker compose up -d`
|
||||
```
|
||||
|
||||
Your keycloak instance is now available on https://doc.yourdomain.tld
|
||||
|
||||
> [!CAUTION]
|
||||
> Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image. You can find available versions on [Keycloak registry](https://quay.io/repository/keycloak/keycloak?tab=tags).
|
||||
```
|
||||
|
||||
## Creating an OIDC Client for Meet Application
|
||||
|
||||
### Step 1: Create a New Realm
|
||||
|
||||
1. Log in to the Keycloak administration console.
|
||||
2. Navigate to the realm tab and click on the "Create realm" button.
|
||||
3. Enter the name of the realm - `meet`.
|
||||
4. Click "Create".
|
||||
|
||||
#### Step 2: Create a New Client
|
||||
|
||||
1. Navigate to the "Clients" tab.
|
||||
2. Click on the "Create client" button.
|
||||
3. Enter the client ID - e.g. `meet`.
|
||||
4. Enable "Client authentication" option.
|
||||
6. Set the "Valid redirect URIs" to the URL of your meet application suffixed with `/*` - e.g., "https://meet.example.com/*".
|
||||
1. Set the "Web Origins" to the URL of your meet application - e.g. `https://meet.example.com`.
|
||||
1. Click "Save".
|
||||
|
||||
#### Step 3: Get Client Credentials
|
||||
|
||||
1. Go to the "Credentials" tab.
|
||||
2. Copy the client ID (`meet` in this example) and the client secret.
|
||||
@@ -1,36 +0,0 @@
|
||||
services:
|
||||
postgresql:
|
||||
image: postgres:16
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
|
||||
interval: 1s
|
||||
timeout: 2s
|
||||
retries: 300
|
||||
env_file:
|
||||
- env.d/kc_postgresql
|
||||
volumes:
|
||||
- ./data/keycloak:/var/lib/postgresql/data/pgdata
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
command: ["start"]
|
||||
env_file:
|
||||
- env.d/kc_postgresql
|
||||
- env.d/keycloak
|
||||
# Uncomment and set your values if using our nginx proxy example
|
||||
# environment:
|
||||
# - VIRTUAL_HOST=id.yourdomain.tld # used by nginx proxy
|
||||
# - VIRTUAL_PORT=8080 # used by nginx proxy
|
||||
# - LETSENCRYPT_HOST=id.yourdomain.tld # used by lets encrypt to generate TLS certificate
|
||||
depends_on:
|
||||
postgresql:
|
||||
condition: service_healthy
|
||||
restart: true
|
||||
# Uncomment if using our nginx proxy example
|
||||
# networks:
|
||||
# - proxy-tier
|
||||
# - default
|
||||
#
|
||||
#networks:
|
||||
# proxy-tier:
|
||||
# external: true
|
||||
@@ -1,39 +0,0 @@
|
||||
# Nginx proxy with automatic SSL certificates
|
||||
|
||||
> [!CAUTION]
|
||||
> We provide those instructions as an example, for extended development or production environments, you should follow the [official documentation](https://github.com/nginx-proxy/acme-companion/tree/main/docs).
|
||||
|
||||
Nginx-proxy sets up a container running nginx and docker-gen. docker-gen generates reverse proxy configs for nginx and reloads nginx when containers are started and stopped.
|
||||
|
||||
Acme-companion is a lightweight companion container for nginx-proxy. It handles the automated creation, renewal and use of SSL certificates for proxied Docker containers through the ACME protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
### Step 1: Prepare your working environment:
|
||||
|
||||
```bash
|
||||
mkdir nginx-proxy && cd nginx-proxy
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/docs/examples/compose/nginx-proxy/compose.yaml
|
||||
```
|
||||
|
||||
### Step 2: Edit `DEFAULT_EMAIL` in the compose file.
|
||||
|
||||
Albeit optional, it is recommended to provide a valid default email address through the `DEFAULT_EMAIL` environment variable, so that Let's Encrypt can warn you about expiring certificates and allow you to recover your account.
|
||||
|
||||
### Step 3: Create docker network
|
||||
|
||||
Containers need share the same network for auto-discovery.
|
||||
|
||||
```bash
|
||||
docker network create proxy-tier
|
||||
```
|
||||
|
||||
### Step 4: Start service
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Once both nginx-proxy and acme-companion containers are up and running, start any container you want proxied with environment variables `VIRTUAL_HOST` and `LETSENCRYPT_HOST` both set to the domain(s) your proxied container is going to use.
|
||||
@@ -1,36 +0,0 @@
|
||||
services:
|
||||
nginx-proxy:
|
||||
image: nginxproxy/nginx-proxy
|
||||
container_name: nginx-proxy
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- html:/usr/share/nginx/html
|
||||
- certs:/etc/nginx/certs:ro
|
||||
- /var/run/docker.sock:/tmp/docker.sock:ro
|
||||
networks:
|
||||
- proxy-tier
|
||||
|
||||
acme-companion:
|
||||
image: nginxproxy/acme-companion
|
||||
container_name: nginx-proxy-acme
|
||||
environment:
|
||||
- DEFAULT_EMAIL=mail@yourdomain.tld
|
||||
volumes_from:
|
||||
- nginx-proxy
|
||||
volumes:
|
||||
- certs:/etc/nginx/certs:rw
|
||||
- acme:/etc/acme.sh
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
networks:
|
||||
- proxy-tier
|
||||
|
||||
networks:
|
||||
proxy-tier:
|
||||
external: true
|
||||
|
||||
volumes:
|
||||
html:
|
||||
certs:
|
||||
acme:
|
||||
@@ -1,22 +0,0 @@
|
||||
port: 7880
|
||||
redis:
|
||||
address: redis:6379
|
||||
keys:
|
||||
meet: <your livekit secret key>
|
||||
# WebRTC configuration
|
||||
rtc:
|
||||
# # when set, LiveKit will attempt to use a UDP mux so all UDP traffic goes through
|
||||
# # listed port(s). To maximize system performance, we recommend using a range of ports
|
||||
# # greater or equal to the number of vCPUs on the machine.
|
||||
# # port_range_start & end must not be set for this config to take effect
|
||||
udp_port: 7882
|
||||
# when set, LiveKit enable WebRTC ICE over TCP when UDP isn't available
|
||||
# this port *cannot* be behind load balancer or TLS, and must be exposed on the node
|
||||
# WebRTC transports are encrypted and do not require additional encryption
|
||||
# only 80/443 on public IP are allowed if less than 1024
|
||||
tcp_port: 7881
|
||||
# use_external_ip should be set to true for most cloud environments where
|
||||
# the host has a public IP address, but is not exposed to the process.
|
||||
# LiveKit will attempt to use STUN to discover the true IP, and advertise
|
||||
# that IP with its clients
|
||||
use_external_ip: true
|
||||
@@ -6,12 +6,11 @@ Here are a bunch of resources to help you install the project.
|
||||
La Suite Meet maintainers use only the Kubernetes deployment method in production, so advanced support is available exclusively for this setup. Please follow the instructions provided [here](/docs/installation/kubernetes.md).
|
||||
|
||||
## Docker Compose
|
||||
We understand that not everyone has a Kubernetes cluster available, please follow the instructions provided [here](/docs/installation/compose.md) to set up a docker compose instance.
|
||||
We understand that not everyone has a Kubernetes cluster available.
|
||||
We also provide [Docker images](https://hub.docker.com/u/lasuite?page=1&search=meet) that can be deployed using Compose.
|
||||
|
||||
## Scalingo
|
||||
|
||||
La Suite Meet can be deployed on Scalingo PaaS using the Suite Numérique buildpack. See the [Scalingo deployment guide](./scalingo.md) for detailed instructions.
|
||||
> [!WARNING]
|
||||
> Under construction: A PR is in progress to support deploying La Suite Meet via Docker Compose.
|
||||
|
||||
## Other ways to install La Suite Meet
|
||||
Community members have contributed alternative ways to install La Suite Meet 🙏. While maintainers may not provide direct support, we help keep these instructions up to date, and you can reach out to contributors or the community for assistance.
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
# Installation with docker compose
|
||||
|
||||
We provide a sample configuration for running Meet using Docker Compose. Please note that this configuration is experimental, and the official way to deploy Meet in production is to use [k8s](../installation/kubernetes.md).
|
||||
|
||||
## Requirements
|
||||
|
||||
All services are required to run the minimalist instance of LaSuite Meet. Click the links for ready-to-use configuration examples:
|
||||
|
||||
| Service | Purpose | Example Config |
|
||||
|-------------------|---------|----------------------------------------------------------|
|
||||
| **PostgreSQL** | Main database | [compose.yaml](../examples/compose/compose.yaml) |
|
||||
| **Redis** | Cache & sessions | [compose.yaml](../examples/compose/compose.yaml) |
|
||||
| **Livekit** | Real-time communication | [compose.yaml](../examples/compose/compose.yaml) |
|
||||
| **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 (MinIO, email). See `/features` folder for details.
|
||||
|
||||
|
||||
## Software Requirements
|
||||
|
||||
Ensure you have Docker Compose(v2) installed on your host server. Follow the official guidelines for a reliable setup:
|
||||
|
||||
Docker Compose is included with Docker Engine:
|
||||
|
||||
- **Docker Engine:** We suggest adhering to the instructions provided by Docker
|
||||
for [installing Docker Engine](https://docs.docker.com/engine/install/).
|
||||
|
||||
For older versions of Docker Engine that do not include Docker Compose:
|
||||
|
||||
- **Docker Compose:** Install it as per the [official documentation](https://docs.docker.com/compose/install/).
|
||||
|
||||
> [!NOTE]
|
||||
> `docker-compose` may not be supported. You are advised to use `docker compose` instead.
|
||||
|
||||
## Step 1: Prepare your working environment:
|
||||
|
||||
```bash
|
||||
mkdir -p meet/env.d && cd meet
|
||||
curl -o compose.yaml https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/docs/examples/compose/compose.yaml
|
||||
curl -o .env https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/env.d/production.dist/hosts
|
||||
curl -o env.d/common https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/env.d/production.dist/common
|
||||
curl -o env.d/postgresql https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/env.d/production.dist/postgresql
|
||||
curl -o livekit-server.yaml https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/docs/examples/livekit/server.yaml
|
||||
curl -o default.conf.template https://raw.githubusercontent.com/suitenumerique/meet/refs/heads/main/docker/files/production/default.conf.template
|
||||
```
|
||||
|
||||
## Step 2: Configuration
|
||||
|
||||
Meet configuration is achieved through environment variables. We provide a [detailed description of all variables](../../src/helm/meet/README.md).
|
||||
|
||||
In this example, we assume the following services:
|
||||
|
||||
- OIDC provider on https://id.yourdomain.tld
|
||||
- Livekit server on https://livekit.yourdomain.tld
|
||||
- Meet server on https://meet.yourdomain.tld
|
||||
|
||||
**Set your own values in `.env`**
|
||||
|
||||
### OIDC
|
||||
|
||||
Authentication in Meet is managed through Open ID Connect protocol. A functional Identity Provider implementing this protocol is required.
|
||||
|
||||
For guidance, refer to our [Keycloak deployment example](../examples/compose/keycloak/README.md).
|
||||
|
||||
If using Keycloak as your Identity Provider, in `env.d/common` set `OIDC_RP_CLIENT_ID` and `OIDC_RP_CLIENT_SECRET` variables with those of the OIDC client created for Meet. By default we have set `meet` as the realm name, if you have named your realm differently, update the value `REALM_NAME` in `.env`
|
||||
|
||||
For others OIDC providers, update the variables in `env.d/common`.
|
||||
|
||||
### Postgresql
|
||||
|
||||
Meet uses PostgreSQL as its database. Although an external PostgreSQL can be used, our example provides a deployment method.
|
||||
|
||||
If you are using the example provided, you need to generate a secure key for `DB_PASSWORD` and set it in `env.d/postgresql`.
|
||||
|
||||
If you are using an external service or not using our default values, you should update the variables in `env.d/postgresql`
|
||||
|
||||
### Redis
|
||||
|
||||
Meet uses Redis for caching and inter-service communication. While an external Redis can be used, our example provides a deployment method.
|
||||
|
||||
If you are using an external service, you need to set `REDIS_URL` environment variable in `env.d/common`.
|
||||
|
||||
### Livekit
|
||||
|
||||
[LiveKit](https://github.com/livekit/livekit) server is used as the WebRTC SFU (Selective Forwarding Unit) allowing multi-user conferencing. For more information, head to [livekit documentation](https://docs.livekit.io/home/self-hosting/).
|
||||
|
||||
Generate a secure key for `LIVEKIT_API_SECRET` in `env.d/common`.
|
||||
|
||||
We provide a minimal recommanded config for production environment in `livekit-server.yaml`. Set the previously generated API secret key in the config file.
|
||||
|
||||
To view other customization options, see [config-sample.yaml](https://github.com/livekit/livekit/blob/master/config-sample.yaml)
|
||||
|
||||
> [!NOTE]
|
||||
> In this example, we configured multiplexing on a single UDP port. For better performances, you can configure a range of UDP ports.
|
||||
|
||||
### Meet
|
||||
|
||||
The Meet backend is built on the Django Framework.
|
||||
|
||||
Generate a [secure key](https://docs.djangoproject.com/en/5.2/ref/settings/#secret-key.) for `DJANGO_SECRET_KEY` in `env.d/common`.
|
||||
|
||||
### Mail
|
||||
|
||||
The following environment variables are required in `env.d/common` for the mail service to send invitations :
|
||||
|
||||
```env
|
||||
DJANGO_EMAIL_HOST=<smtp host>
|
||||
DJANGO_EMAIL_HOST_USER=<smtp user>
|
||||
DJANGO_EMAIL_HOST_PASSWORD=<smtp password>
|
||||
DJANGO_EMAIL_PORT=<smtp port>
|
||||
DJANGO_EMAIL_FROM=<your email address>
|
||||
|
||||
#DJANGO_EMAIL_USE_TLS=true # A flag to enable or disable TLS for email sending.
|
||||
#DJANGO_EMAIL_USE_SSL=true # A flag to enable or disable SSL for email sending.
|
||||
|
||||
|
||||
DJANGO_EMAIL_BRAND_NAME=<brand name used in email templates> # e.g. "La Suite Numérique"
|
||||
DJANGO_EMAIL_LOGO_IMG=<logo image to use in email templates.> # e.g. "https://meet.yourdomain.tld/assets/logo-suite-numerique.png"
|
||||
```
|
||||
|
||||
## Step 3: Configure your firewall
|
||||
|
||||
If you are using a firewall as it is usually recommended in a production environment you will need to allow the webservice traffic on ports 80 and 443 but also to allow UDP traffic for the WebRTC service.
|
||||
|
||||
The following ports will need to be opened:
|
||||
- 80/tcp - for TLS issuance
|
||||
- 443/tcp - for listening on HTTPS and TURN/TLS packets
|
||||
- 7881/tcp - WebRTC ICE over TCP
|
||||
- 7882/udp - for WebRTC multiplexing over UDP
|
||||
|
||||
If you are using ufw, enter the following:
|
||||
```
|
||||
ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw allow 443/udp
|
||||
ufw allow 7881/tcp
|
||||
ufw allow 7882/udp
|
||||
ufw enable
|
||||
```
|
||||
|
||||
## Step 4: Reverse proxy and SSL/TLS
|
||||
|
||||
> [!WARNING]
|
||||
> In a production environment, configure SSL/TLS termination to run your instance on https.
|
||||
|
||||
If you have your own certificates and proxy setup, you can skip this part.
|
||||
|
||||
You can follow our [nginx proxy example](../examples/compose/nginx-proxy/README.md) with automatic generation and renewal of certificate with Let's Encrypt.
|
||||
|
||||
You will need to uncomment the environment and network sections in compose file and update it with your values.
|
||||
|
||||
```yaml
|
||||
frontend:
|
||||
...
|
||||
# Uncomment and set your values if using our nginx proxy example
|
||||
# environment:
|
||||
# - VIRTUAL_HOST=${MEET_HOST} # used by nginx proxy
|
||||
# - VIRTUAL_PORT=8083 # used by nginx proxy
|
||||
# - LETSENCRYPT_HOST=${MEET_HOST} # used by lets encrypt to generate TLS certificate
|
||||
...
|
||||
# Uncomment if using our nginx proxy example
|
||||
# networks:
|
||||
# - proxy-tier
|
||||
# - default
|
||||
...
|
||||
# environment:
|
||||
# - VIRTUAL_HOST=${LIVEKIT_HOST} # used by nginx proxy
|
||||
# - VIRTUAL_PORT=7880 # used by nginx proxy
|
||||
# - LETSENCRYPT_HOST=${LIVEKIT_HOST} # used by lets encrypt to generate TLS certificate
|
||||
# Uncomment if using our nginx proxy example
|
||||
# networks:
|
||||
# - proxy-tier
|
||||
# - default
|
||||
#networks:
|
||||
# proxy-tier:
|
||||
# external: true
|
||||
```
|
||||
|
||||
#### Caddy Reverse Proxy
|
||||
Expose the Frontend port to the host
|
||||
```yaml
|
||||
frontend:
|
||||
…
|
||||
ports:
|
||||
- "8086:8086"
|
||||
```
|
||||
|
||||
## Step 5: Start Meet
|
||||
|
||||
You are ready to start your Meet application !
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
> [!NOTE]
|
||||
> Version of the images are set to latest, you should pin it to the desired version to avoid unwanted upgrades when pulling latest image.
|
||||
|
||||
## Step 6: Run the database migration and create Django admin user
|
||||
|
||||
```bash
|
||||
docker compose run --rm backend python manage.py migrate
|
||||
docker compose run --rm backend python manage.py createsuperuser --email <admin email> --password <admin password>
|
||||
```
|
||||
|
||||
Replace `<admin email>` with the email of your admin user and generate a secure password.
|
||||
|
||||
Your Meet instance is now available on the domain you defined, https://meet.yourdomain.tld.
|
||||
|
||||
The admin interface is available on https://meet.yourdomain.tld/admin with the admin user you just created.
|
||||
|
||||
## How to upgrade your Meet application
|
||||
|
||||
Before running an upgrade you must check the [Upgrade document](../../UPGRADE.md) for specific procedures that might be needed.
|
||||
|
||||
You can also check the [Changelog](../../CHANGELOG.md) for brief summary of the changes.
|
||||
|
||||
### Step 1: Edit the images tag with the desired version
|
||||
|
||||
### Step 2: Pull the images
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
```
|
||||
|
||||
### Step 3: Restart your containers
|
||||
|
||||
```bash
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
### Step 4: Run the database migration
|
||||
Your database schema may need to be updated, run:
|
||||
```bash
|
||||
docker compose run --rm backend python manage.py migrate
|
||||
```
|
||||
@@ -277,6 +277,7 @@ These are the environmental options available on meet backend.
|
||||
| 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_TRANSCRIPT | Frontend transcription configuration, you can pass a beta form, with `form_beta_users` | {} |
|
||||
| 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 |
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
# Deployment on Scalingo
|
||||
|
||||
This guide explains how to deploy La Suite Meet on [Scalingo](https://scalingo.com/) using the [Suite Numérique buildpack](https://github.com/suitenumerique/buildpack).
|
||||
|
||||
## Overview
|
||||
|
||||
Scalingo is a Platform-as-a-Service (PaaS) that simplifies application deployment. This setup uses a custom buildpack to handle both the frontend (Vite) and backend (Django) builds, serving them through Nginx.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A Scalingo account
|
||||
- Scalingo CLI installed (optional but recommended)
|
||||
- A PostgreSQL database addon
|
||||
- A Redis addon (for caching and sessions)
|
||||
|
||||
## Step 1: Create Your App
|
||||
|
||||
Create a new app on Scalingo using `scalingo` cli or using the [Scalingo dashboard](https://dashboard.scalingo.com/).
|
||||
|
||||
## Step 2: Provision Addons
|
||||
|
||||
Add the required PostgreSQL and Redis services.
|
||||
|
||||
This will set the following environment variables automatically:
|
||||
- `SCALINGO_POSTGRESQL_URL` - Database connection string
|
||||
- `SCALINGO_REDIS_URL` - Redis connection string
|
||||
|
||||
## Step 3: Configure Environment Variables
|
||||
|
||||
Set the following environment variables in your Scalingo app:
|
||||
|
||||
### Buildpack Configuration
|
||||
|
||||
```bash
|
||||
scalingo env-set BUILDBACK_URL="https://github.com/suitenumerique/buildpack#main"
|
||||
scalingo env-set LASUITE_APP_NAME="meet"
|
||||
scalingo env-set LASUITE_BACKEND_DIR="."
|
||||
scalingo env-set LASUITE_FRONTEND_DIR="src/frontend/"
|
||||
scalingo env-set LASUITE_NGINX_DIR="."
|
||||
scalingo env-set LASUITE_SCRIPT_POSTCOMPILE="bin/buildpack_postcompile.sh"
|
||||
scalingo env-set LASUITE_SCRIPT_POSTFRONTEND="bin/buildpack_postfrontend.sh"
|
||||
```
|
||||
|
||||
### Database and Cache
|
||||
|
||||
```bash
|
||||
scalingo env-set DATABASE_URL="\$SCALINGO_POSTGRESQL_URL"
|
||||
scalingo env-set REDIS_URL="\$SCALINGO_REDIS_URL"
|
||||
```
|
||||
|
||||
### Django Settings
|
||||
|
||||
```bash
|
||||
scalingo env-set DJANGO_SETTINGS_MODULE="meet.settings"
|
||||
scalingo env-set DJANGO_CONFIGURATION="Production"
|
||||
scalingo env-set DJANGO_SECRET_KEY="<generate-a-secure-secret-key>"
|
||||
scalingo env-set DJANGO_ALLOWED_HOSTS="my-meet-app.osc-fr1.scalingo.io"
|
||||
```
|
||||
|
||||
### OIDC Authentication
|
||||
|
||||
Configure your OIDC provider (e.g., Keycloak, Authentik):
|
||||
|
||||
```bash
|
||||
scalingo env-set OIDC_OP_BASE_URL="https://auth.yourdomain.com/realms/meet"
|
||||
scalingo env-set OIDC_RP_CLIENT_ID="meet-client-id"
|
||||
scalingo env-set OIDC_RP_CLIENT_SECRET="<your-client-secret>"
|
||||
scalingo env-set OIDC_RP_SIGN_ALGO="RS256"
|
||||
```
|
||||
|
||||
### LiveKit Configuration
|
||||
|
||||
Meet requires a LiveKit server for video conferencing:
|
||||
|
||||
```bash
|
||||
scalingo env-set LIVEKIT_API_URL="wss://livekit.yourdomain.com"
|
||||
scalingo env-set LIVEKIT_API_KEY="<your-livekit-api-key>"
|
||||
scalingo env-set LIVEKIT_API_SECRET="<your-livekit-api-secret>"
|
||||
```
|
||||
|
||||
### Email Configuration (Optional)
|
||||
|
||||
For email notifications see https://doc.scalingo.com/platform/app/sending-emails:
|
||||
|
||||
```bash
|
||||
scalingo env-set DJANGO_EMAIL_HOST="smtp.example.org"
|
||||
scalingo env-set DJANGO_EMAIL_PORT="587"
|
||||
scalingo env-set DJANGO_EMAIL_HOST_USER="<smtp-user>"
|
||||
scalingo env-set DJANGO_EMAIL_HOST_PASSWORD="<smtp-password>"
|
||||
scalingo env-set DJANGO_EMAIL_USE_TLS="True"
|
||||
scalingo env-set DJANGO_EMAIL_FROM="meet@yourdomain.com"
|
||||
```
|
||||
|
||||
## Step 4: Deploy
|
||||
|
||||
Deploy your application:
|
||||
|
||||
```bash
|
||||
git push scalingo main
|
||||
```
|
||||
|
||||
The Procfile will automatically:
|
||||
1. Build the frontend (Vite)
|
||||
2. Build the backend (Django)
|
||||
3. Run the post-compile script (cleanup)
|
||||
4. Run the post-frontend script (move assets and prepare for deployment)
|
||||
5. Start Nginx and Gunicorn
|
||||
6. Run django migrations
|
||||
|
||||
## Step 5: Create superuser
|
||||
|
||||
After the first deployment, create an admin user:
|
||||
|
||||
```bash
|
||||
scalingo run python manage.py createsuperuser
|
||||
```
|
||||
|
||||
## Custom Domain (Optional)
|
||||
|
||||
To use a custom domain:
|
||||
|
||||
1. Add the domain in Scalingo dashboard
|
||||
2. Update `DJANGO_ALLOWED_HOSTS` with your custom domain
|
||||
3. Configure your DNS to point to Scalingo
|
||||
|
||||
```bash
|
||||
scalingo domains-add meet.yourdomain.com
|
||||
scalingo env-set DJANGO_ALLOWED_HOSTS="meet.yourdomain.com,my-meet-app.osc-fr1.scalingo.io"
|
||||
```
|
||||
|
||||
## Custom Logo (Optional)
|
||||
|
||||
To use a custom logo, set the `CUSTOM_LOGO_URL` environment variable with an HTTPS URL pointing to an SVG item (max 5MB):
|
||||
|
||||
```bash
|
||||
scalingo env-set CUSTOM_LOGO_URL="https://cdn.yourdomain.com/logo.svg"
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Logs
|
||||
|
||||
```bash
|
||||
scalingo logs --tail
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Build fails**: Check that all required environment variables are set
|
||||
2. **Database connection error**: Verify `DATABASE_URL` is correctly set to `$SCALINGO_POSTGRESQL_URL`
|
||||
3. **Static files not served**: Ensure the buildpack post-frontend script ran successfully
|
||||
4. **OIDC errors**: Verify your OIDC provider configuration and callback URLs
|
||||
|
||||
### Useful Commands
|
||||
|
||||
```bash
|
||||
# Open a console
|
||||
scalingo run bash
|
||||
|
||||
# Restart the app
|
||||
scalingo restart
|
||||
|
||||
# Scale containers
|
||||
scalingo scale web:2
|
||||
|
||||
# One-off command
|
||||
scalingo run python manage.py shell
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
On Scalingo, the application runs as follows:
|
||||
|
||||
1. **Build Phase**: The buildpack compiles both frontend and backend
|
||||
2. **Runtime**:
|
||||
- Nginx serves static files and proxies to the backend
|
||||
- Gunicorn runs the Django WSGI application
|
||||
- Both processes are managed by the `bin/buildpack_start.sh` script
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Scalingo Documentation](https://doc.scalingo.com/)
|
||||
- [Suite Numérique Buildpack](https://github.com/suitenumerique/buildpack)
|
||||
- [Meet Environment Variables](../../src/helm/meet/README.md)
|
||||
- [Django Configurations Documentation](https://django-configurations.readthedocs.io/)
|
||||
+4
-3
@@ -7,7 +7,7 @@ info:
|
||||
|
||||
#### Authentication Flow
|
||||
|
||||
1. Exchange application credentials for a JWT token via `/external-api/v1.0/application/token/`.
|
||||
1. Exchange application credentials for a JWT token via `/external-api/v1.0/applications/token`.
|
||||
2. Use the JWT token in the `Authorization: Bearer <token>` header for all subsequent requests.
|
||||
3. Tokens are scoped and allow applications to act on behalf of specific users.
|
||||
|
||||
@@ -21,6 +21,7 @@ info:
|
||||
|
||||
#### Upcoming Features
|
||||
|
||||
* **Create rooms for unknown users from the web app:** Support for generating rooms for users who are not yet registered in the system.
|
||||
* **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.
|
||||
|
||||
@@ -39,7 +40,7 @@ tags:
|
||||
description: Room management operations
|
||||
|
||||
paths:
|
||||
/application/token/:
|
||||
/applications/token:
|
||||
post:
|
||||
tags:
|
||||
- Authentication
|
||||
@@ -282,7 +283,7 @@ components:
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT token obtained from the `/application/token/` endpoint.
|
||||
JWT token obtained from the `/applications/token` endpoint.
|
||||
Include in requests as: `Authorization: Bearer <token>`
|
||||
|
||||
schemas:
|
||||
|
||||
@@ -1,393 +0,0 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Meet External API
|
||||
version: 1.0.0
|
||||
description: |
|
||||
External API for room management with resource server authentication.
|
||||
[[description by Oauth 2.0]](https://www.oauth.com/oauth2-servers/the-resource-server/)
|
||||
|
||||
#### Authentication Flow
|
||||
|
||||
1. Authenticate with the authorization server using your credentials
|
||||
2. During authentication, request the scopes you need: `lasuite_visio` (mandatory) plus action-specific scopes
|
||||
3. Receive an access token and a refresh token that includes the requested scopes
|
||||
4. Use the access token in the `Authorization: Bearer <token>` header for all API requests
|
||||
5. When the access token expires, use the refresh token to obtain a new access token without re-authenticating
|
||||
|
||||
#### Scopes
|
||||
|
||||
* `lasuite_visio` - **Mandatory** Base scope required for any API access
|
||||
* `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` – **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
|
||||
|
||||
* **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.
|
||||
|
||||
contact:
|
||||
name: API Support
|
||||
email: antoine.lebaud@mail.numerique.gouv.fr
|
||||
|
||||
servers:
|
||||
- url: https://visio-sandbox.beta.numerique.gouv.fr/external-api/v1.0
|
||||
description: Sandbox server
|
||||
|
||||
tags:
|
||||
- name: Rooms
|
||||
description: Room management operations
|
||||
|
||||
paths:
|
||||
/rooms:
|
||||
get:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: List rooms
|
||||
description: |
|
||||
Returns a list of rooms accessible to the authenticated user.
|
||||
Only rooms where the delegated user has access will be returned.
|
||||
operationId: listRooms
|
||||
security:
|
||||
- BearerAuth: [rooms:list]
|
||||
parameters:
|
||||
- name: page
|
||||
in: query
|
||||
description: Page number for pagination
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
default: 1
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Number of items per page
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
responses:
|
||||
'200':
|
||||
description: List of accessible rooms
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
count:
|
||||
type: integer
|
||||
description: Total number of rooms
|
||||
next:
|
||||
type: string
|
||||
nullable: true
|
||||
description: URL to next page
|
||||
previous:
|
||||
type: string
|
||||
nullable: true
|
||||
description: URL to previous page
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Room'
|
||||
examples:
|
||||
roomList:
|
||||
summary: Paginated room list
|
||||
value:
|
||||
count: 2
|
||||
next: "https://visio-sandbox.beta.numerique.gouv.fr/external-api/v1.0/rooms?page=2"
|
||||
previous: null
|
||||
results:
|
||||
- id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||||
slug: "aae-erez-aaz"
|
||||
access_level: "trusted"
|
||||
url: "https://visio-sandbox.beta.numerique.gouv.fr/aae-erez-aaz"
|
||||
telephony:
|
||||
enabled: true
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
|
||||
post:
|
||||
tags:
|
||||
- Rooms
|
||||
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:**
|
||||
- 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
|
||||
operationId: createRoom
|
||||
security:
|
||||
- BearerAuth: [rooms:create]
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RoomCreate'
|
||||
examples:
|
||||
emptyBody:
|
||||
summary: No parameters (default)
|
||||
value: {}
|
||||
responses:
|
||||
'201':
|
||||
description: Room created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Room'
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
|
||||
/rooms/{id}:
|
||||
get:
|
||||
tags:
|
||||
- Rooms
|
||||
summary: Retrieve a room
|
||||
description: Get detailed information about a specific room by its ID
|
||||
operationId: retrieveRoom
|
||||
security:
|
||||
- BearerAuth: [rooms:retrieve]
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Room UUID
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
responses:
|
||||
'200':
|
||||
description: Room details
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Room'
|
||||
examples:
|
||||
room:
|
||||
summary: Room details
|
||||
value:
|
||||
id: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||||
slug: "aae-erez-aaz"
|
||||
access_level: "trusted"
|
||||
url: "https://visio-sandbox.beta.numerique.gouv.fr/aae-erez-aaz"
|
||||
telephony:
|
||||
enabled: true
|
||||
pin_code: "123456"
|
||||
phone_number: "+1-555-0100"
|
||||
default_country: "US"
|
||||
'401':
|
||||
$ref: '#/components/responses/UnauthorizedError'
|
||||
'403':
|
||||
$ref: '#/components/responses/ForbiddenError'
|
||||
'404':
|
||||
$ref: '#/components/responses/RoomNotFoundError'
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
BearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
JWT token obtained from the `/application/token` endpoint.
|
||||
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: Empty object - all room properties are auto-generated
|
||||
properties: {}
|
||||
|
||||
Room:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
description: Unique room identifier
|
||||
example: "7c9e6679-7425-40de-944b-e07fc1f90ae7"
|
||||
slug:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: URL-friendly room identifier (auto-generated)
|
||||
example: "aze-eere-zer"
|
||||
access_level:
|
||||
type: string
|
||||
readOnly: true
|
||||
description: Room access level (always 'trusted' for API-created rooms)
|
||||
example: "trusted"
|
||||
url:
|
||||
type: string
|
||||
format: uri
|
||||
readOnly: true
|
||||
description: Full URL to access the room
|
||||
example: "https://visio-sandbox.beta.numerique.gouv.fr/aze-eere-zer"
|
||||
telephony:
|
||||
type: object
|
||||
readOnly: true
|
||||
description: Telephony dial-in information (if enabled)
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
description: Whether telephony is available
|
||||
example: true
|
||||
pin_code:
|
||||
type: string
|
||||
description: PIN code for dial-in access
|
||||
example: "123456"
|
||||
phone_number:
|
||||
type: string
|
||||
description: Phone number to dial
|
||||
example: "+1-555-0100"
|
||||
default_country:
|
||||
type: string
|
||||
description: Default country code
|
||||
example: "US"
|
||||
|
||||
OAuthError:
|
||||
type: object
|
||||
description: OAuth2-compliant error response
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Human-readable error description
|
||||
example: "Invalid credentials"
|
||||
|
||||
Error:
|
||||
type: object
|
||||
properties:
|
||||
detail:
|
||||
type: string
|
||||
description: Error message
|
||||
example: "Invalid token."
|
||||
|
||||
ValidationError:
|
||||
type: object
|
||||
properties:
|
||||
field_name:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of validation errors for this field
|
||||
example: ["This field is required."]
|
||||
|
||||
responses:
|
||||
UnauthorizedError:
|
||||
description: The access token is expired, revoked, malformed, or invalid for other reasons. The client can obtain a new access token and try again.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
invalidToken:
|
||||
summary: Invalid token
|
||||
value:
|
||||
error: "Invalid token."
|
||||
|
||||
ForbiddenError:
|
||||
description: Insufficient scope for this operation
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
insufficientScope:
|
||||
summary: Missing required scope
|
||||
value:
|
||||
detail: "Insufficient permissions. Required scope: 'rooms:xxxx'"
|
||||
|
||||
RoomNotFoundError:
|
||||
description: Room not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
examples:
|
||||
roomNotFound:
|
||||
summary: Room does not exist
|
||||
value:
|
||||
detail: "Not found."
|
||||
|
||||
BadRequestError:
|
||||
description: Invalid request data
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
examples:
|
||||
validationError:
|
||||
summary: Field validation failed
|
||||
value:
|
||||
scope: ["Invalid email address."]
|
||||
@@ -32,8 +32,6 @@ OIDC_OP_JWKS_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/cert
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT=http://localhost:8083/realms/meet/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/userinfo
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT=http://nginx:8083/realms/meet/protocol/openid-connect/token/introspect
|
||||
OIDC_OP_URL=http://localhost:8083/realms/meet
|
||||
|
||||
OIDC_RP_CLIENT_ID=meet
|
||||
OIDC_RP_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
@@ -47,9 +45,6 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=localhost:8083,localhost:3000
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
|
||||
|
||||
OIDC_RS_CLIENT_ID=meet
|
||||
OIDC_RS_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
|
||||
|
||||
# Livekit Token settings
|
||||
LIVEKIT_API_SECRET=secret
|
||||
LIVEKIT_API_KEY=devkey
|
||||
@@ -63,7 +58,7 @@ 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
|
||||
RECORDING_DOWNLOAD_BASE_URL=http://localhost:3000/recording
|
||||
SCREEN_RECORDING_BASE_URL=http://localhost:3000/recordings
|
||||
|
||||
# Telephony
|
||||
ROOM_TELEPHONY_ENABLED=True
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
APP_NAME="meet-app-summary-dev"
|
||||
APP_API_TOKEN="password"
|
||||
|
||||
AWS_STORAGE_BUCKET_NAME="http://meet-media-storage"
|
||||
AWS_STORAGE_BUCKET_NAME="meet-media-storage"
|
||||
AWS_S3_ENDPOINT_URL="minio:9000"
|
||||
AWS_S3_SECURE_ACCESS=false
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
# Django
|
||||
DJANGO_ALLOWED_HOSTS=${MEET_HOST}
|
||||
DJANGO_SECRET_KEY=<generate a secret key>
|
||||
DJANGO_SETTINGS_MODULE=meet.settings
|
||||
DJANGO_CONFIGURATION=Production
|
||||
|
||||
# Python
|
||||
PYTHONPATH=/app
|
||||
|
||||
# Meet settings
|
||||
|
||||
# Mail
|
||||
DJANGO_EMAIL_HOST=<smtp host>
|
||||
DJANGO_EMAIL_HOST_USER=<smtp user>
|
||||
DJANGO_EMAIL_HOST_PASSWORD=<smtp password>
|
||||
DJANGO_EMAIL_PORT=<smtp port>
|
||||
DJANGO_EMAIL_FROM=<your email address>
|
||||
|
||||
#DJANGO_EMAIL_USE_TLS=true # A flag to enable or disable TLS for email sending.
|
||||
#DJANGO_EMAIL_USE_SSL=true # A flag to enable or disable SSL for email sending.
|
||||
|
||||
DJANGO_EMAIL_BRAND_NAME="La Suite Numérique"
|
||||
DJANGO_EMAIL_LOGO_IMG="https://${MEET_HOST}/assets/logo-suite-numerique.png"
|
||||
|
||||
# Backend url
|
||||
MEET_BASE_URL="https://${MEET_HOST}"
|
||||
|
||||
# OIDC
|
||||
OIDC_OP_JWKS_ENDPOINT=https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/certs
|
||||
OIDC_OP_AUTHORIZATION_ENDPOINT=https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/auth
|
||||
OIDC_OP_TOKEN_ENDPOINT=https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/token
|
||||
OIDC_OP_USER_ENDPOINT=https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/userinfo
|
||||
OIDC_OP_LOGOUT_ENDPOINT=https://${KEYCLOAK_HOST}/realms/${REALM_NAME}/protocol/openid-connect/logout
|
||||
|
||||
OIDC_RP_CLIENT_ID=<client_id>
|
||||
OIDC_RP_CLIENT_SECRET=<client secret>
|
||||
OIDC_RP_SIGN_ALGO=RS256
|
||||
OIDC_RP_SCOPES="openid email"
|
||||
|
||||
LOGIN_REDIRECT_URL=https://${MEET_HOST}
|
||||
LOGIN_REDIRECT_URL_FAILURE=https://${MEET_HOST}
|
||||
LOGOUT_REDIRECT_URL=https://${MEET_HOST}
|
||||
|
||||
OIDC_REDIRECT_ALLOWED_HOSTS=["https://${MEET_HOST}"]
|
||||
|
||||
# Livekit Token settings
|
||||
LIVEKIT_API_SECRET=<generate a secret key>
|
||||
LIVEKIT_API_KEY=meet
|
||||
LIVEKIT_API_URL=https://${LIVEKIT_HOST}
|
||||
ALLOW_UNREGISTERED_ROOMS=False
|
||||
@@ -1,7 +0,0 @@
|
||||
MEET_HOST=meet.domain.tld
|
||||
KEYCLOAK_HOST=id.domain.tld
|
||||
LIVEKIT_HOST=livekit.domain.tld
|
||||
BACKEND_INTERNAL_HOST=backend
|
||||
FRONTEND_INTERNAL_HOST=frontend
|
||||
LIVEKIT_INTERNAL_HOST=livekit
|
||||
REALM_NAME=meet
|
||||
@@ -1,13 +0,0 @@
|
||||
# Postgresql db container configuration
|
||||
POSTGRES_DB=keycloak
|
||||
POSTGRES_USER=keycloak
|
||||
POSTGRES_PASSWORD=<generate postgres password>
|
||||
PGDATA=/var/lib/postgresql/data/pgdata
|
||||
|
||||
# Keycloak postgresql configuration
|
||||
KC_DB=postgres
|
||||
KC_DB_SCHEMA=public
|
||||
KC_DB_URL_HOST=postgresql
|
||||
KC_DB_NAME=${POSTGRES_DB}
|
||||
KC_DB_USER=${POSTGRES_USER}
|
||||
KC_DB_PASSWORD=${POSTGRES_PASSWORD}
|
||||
@@ -1,8 +0,0 @@
|
||||
# Keycloak admin user
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME=admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD=<generate your password>
|
||||
|
||||
# Keycloak configuration
|
||||
KC_HOSTNAME=https://id.yourdomain.tld # Change with your own URL
|
||||
KC_PROXY_HEADERS=xforwarded # in this example we are running behind an nginx proxy
|
||||
KC_HTTP_ENABLED=true # in this example we are running behind an nginx proxy
|
||||
@@ -1,11 +0,0 @@
|
||||
# App database configuration
|
||||
DB_HOST=postgresql
|
||||
DB_NAME=meet
|
||||
DB_USER=meet
|
||||
DB_PASSWORD=<generate a secure password>
|
||||
DB_PORT=5432
|
||||
|
||||
# Postgresql db container configuration
|
||||
POSTGRES_DB=meet
|
||||
POSTGRES_USER=meet
|
||||
POSTGRES_PASSWORD=${DB_PASSWORD}
|
||||
@@ -9,12 +9,6 @@
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["redis"]
|
||||
},
|
||||
{
|
||||
"groupName": "allowed pylint versions",
|
||||
"matchManagers": ["pep621"],
|
||||
"matchPackageNames": ["pylint"],
|
||||
"allowedVersions": "<4.0.0"
|
||||
},
|
||||
{
|
||||
"enabled": false,
|
||||
"groupName": "ignored js dependencies",
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
FROM python:3.13-slim AS base
|
||||
|
||||
# Install system dependencies required by LiveKit
|
||||
RUN apt-get update && apt-get install -y \
|
||||
libglib2.0-0 \
|
||||
libgobject-2.0-0 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
FROM base AS builder
|
||||
|
||||
WORKDIR /builder
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
import os
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from lasuite.plugins import kyutai
|
||||
from livekit import api, rtc
|
||||
from livekit.agents import (
|
||||
Agent,
|
||||
@@ -14,15 +13,14 @@ from livekit.agents import (
|
||||
JobContext,
|
||||
JobProcess,
|
||||
JobRequest,
|
||||
RoomInputOptions,
|
||||
RoomIO,
|
||||
RoomOutputOptions,
|
||||
WorkerOptions,
|
||||
WorkerPermissions,
|
||||
cli,
|
||||
utils,
|
||||
)
|
||||
from livekit.agents import (
|
||||
room_io as lk_room_io,
|
||||
)
|
||||
from livekit.plugins import deepgram, silero
|
||||
|
||||
load_dotenv()
|
||||
@@ -30,26 +28,6 @@ load_dotenv()
|
||||
logger = logging.getLogger("transcriber")
|
||||
|
||||
TRANSCRIBER_AGENT_NAME = os.getenv("TRANSCRIBER_AGENT_NAME", "multi-user-transcriber")
|
||||
STT_PROVIDER = os.getenv("STT_PROVIDER", "deepgram")
|
||||
ENABLE_SILERO_VAD = os.getenv("ENABLE_SILERO_VAD", "true").lower() == "true"
|
||||
|
||||
|
||||
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
|
||||
# Use language="multi" instead for automatic multilingual support
|
||||
_stt_instance = deepgram.STT(
|
||||
model=os.getenv("DEEPGRAM_STT_MODEL", "nova-3"),
|
||||
language=os.getenv("DEEPGRAM_STT_LANGUAGE", "multi"),
|
||||
)
|
||||
elif STT_PROVIDER == "kyutai":
|
||||
_stt_instance = kyutai.STT(base_url=os.getenv("KYUTAI_STT_BASE_URL"))
|
||||
else:
|
||||
raise ValueError(f"Unknown STT_PROVIDER: {STT_PROVIDER}")
|
||||
|
||||
return _stt_instance
|
||||
|
||||
|
||||
class Transcriber(Agent):
|
||||
@@ -57,11 +35,9 @@ class Transcriber(Agent):
|
||||
|
||||
def __init__(self, *, participant_identity: str):
|
||||
"""Init transcription agent."""
|
||||
stt = create_stt_provider()
|
||||
|
||||
super().__init__(
|
||||
instructions="not-needed",
|
||||
stt=stt,
|
||||
stt=deepgram.STT(),
|
||||
)
|
||||
self.participant_identity = participant_identity
|
||||
|
||||
@@ -123,14 +99,19 @@ class MultiUserTranscriber:
|
||||
if participant.identity in self._sessions:
|
||||
return self._sessions[participant.identity]
|
||||
|
||||
vad = self.ctx.proc.userdata.get("vad", None)
|
||||
session = AgentSession(vad=vad)
|
||||
session = AgentSession(
|
||||
vad=self.ctx.proc.userdata["vad"],
|
||||
)
|
||||
room_io = RoomIO(
|
||||
agent_session=session,
|
||||
room=self.ctx.room,
|
||||
participant=participant,
|
||||
options=lk_room_io.RoomOptions(
|
||||
text_input=False, audio_output=False, text_output=True
|
||||
input_options=RoomInputOptions(
|
||||
text_enabled=False,
|
||||
),
|
||||
output_options=RoomOutputOptions(
|
||||
transcription_enabled=True,
|
||||
audio_enabled=False,
|
||||
),
|
||||
)
|
||||
await room_io.start()
|
||||
@@ -193,8 +174,7 @@ async def handle_transcriber_job_request(job_req: JobRequest) -> None:
|
||||
|
||||
def prewarm(proc: JobProcess):
|
||||
"""Preload voice activity detection model."""
|
||||
if ENABLE_SILERO_VAD:
|
||||
proc.userdata["vad"] = silero.VAD.load()
|
||||
proc.userdata["vad"] = silero.VAD.load()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
|
||||
[project]
|
||||
name = "agents"
|
||||
version = "1.6.0"
|
||||
version = "0.1.40"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"livekit-agents==1.3.10",
|
||||
"livekit-plugins-deepgram==1.3.10",
|
||||
"livekit-plugins-silero==1.3.10",
|
||||
"livekit-plugins-kyutai-lasuite==0.0.6",
|
||||
"python-dotenv==1.2.1"
|
||||
"livekit-agents==1.2.6",
|
||||
"livekit-plugins-deepgram==1.2.6",
|
||||
"livekit-plugins-silero==1.2.6",
|
||||
"python-dotenv==1.1.1"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"ruff==0.14.4",
|
||||
"ruff==0.12.0",
|
||||
]
|
||||
|
||||
[build-system]
|
||||
|
||||
+5
-100
@@ -1,12 +1,10 @@
|
||||
"""Admin classes and registrations for core app."""
|
||||
|
||||
from django import forms
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib import admin
|
||||
from django.contrib.auth import admin as auth_admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from core.recording.event import notification
|
||||
|
||||
from . import models
|
||||
|
||||
|
||||
@@ -111,90 +109,16 @@ class RoomAdmin(admin.ModelAdmin):
|
||||
|
||||
inlines = (ResourceAccessInline,)
|
||||
search_fields = ["name", "slug", "=id"]
|
||||
list_display = ["name", "slug", "access_level", "get_owner", "created_at"]
|
||||
list_display = ["name", "slug", "access_level", "created_at"]
|
||||
list_filter = ["access_level", "created_at"]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
|
||||
return super().get_queryset(request).prefetch_related("accesses__user")
|
||||
|
||||
def get_owner(self, obj):
|
||||
"""Return the owner of the room for display in the admin list."""
|
||||
|
||||
owners = [
|
||||
access
|
||||
for access in obj.accesses.all()
|
||||
if access.role == models.RoleChoices.OWNER
|
||||
]
|
||||
|
||||
if not owners:
|
||||
return _("No owner")
|
||||
|
||||
if len(owners) > 1:
|
||||
return _("Multiple owners")
|
||||
|
||||
return str(owners[0].user)
|
||||
|
||||
|
||||
class RecordingAccessInline(admin.TabularInline):
|
||||
"""Inline admin class for recording accesses."""
|
||||
|
||||
model = models.RecordingAccess
|
||||
extra = 0
|
||||
autocomplete_fields = ["user"]
|
||||
|
||||
|
||||
@admin.action(description=_("Resend notification to external service"))
|
||||
def resend_notification(modeladmin, request, queryset): # pylint: disable=unused-argument
|
||||
"""Resend notification to external service for selected recordings."""
|
||||
|
||||
notification_service = notification.NotificationService()
|
||||
processed = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
|
||||
for recording in queryset:
|
||||
if recording.is_expired:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
try:
|
||||
success = notification_service.notify_external_services(recording)
|
||||
|
||||
if success:
|
||||
processed += 1
|
||||
else:
|
||||
failed += 1
|
||||
modeladmin.message_user(
|
||||
request,
|
||||
_("Failed to notify for recording %(id)s") % {"id": recording.id},
|
||||
level=messages.ERROR,
|
||||
)
|
||||
|
||||
except Exception as e: # noqa: BLE001 # pylint: disable=broad-except
|
||||
failed += 1
|
||||
modeladmin.message_user(
|
||||
request,
|
||||
_("Failed to notify for recording %(id)s: %(error)s")
|
||||
% {"id": recording.id, "error": str(e)},
|
||||
level=messages.ERROR,
|
||||
)
|
||||
|
||||
if processed > 0:
|
||||
modeladmin.message_user(
|
||||
request,
|
||||
_("Successfully sent notifications for %(count)s recording(s).")
|
||||
% {"count": processed},
|
||||
level=messages.SUCCESS,
|
||||
)
|
||||
|
||||
if skipped > 0:
|
||||
modeladmin.message_user(
|
||||
request,
|
||||
_("Skipped %(count)s expired recording(s).") % {"count": skipped},
|
||||
level=messages.WARNING,
|
||||
)
|
||||
|
||||
|
||||
@admin.register(models.Recording)
|
||||
@@ -203,28 +127,9 @@ class RecordingAdmin(admin.ModelAdmin):
|
||||
|
||||
inlines = (RecordingAccessInline,)
|
||||
search_fields = ["status", "=id", "worker_id", "room__slug", "=room__id"]
|
||||
list_display = (
|
||||
"id",
|
||||
"status",
|
||||
"mode",
|
||||
"room",
|
||||
"get_owner",
|
||||
"created_at",
|
||||
"worker_id",
|
||||
)
|
||||
list_filter = ["created_at"]
|
||||
list_select_related = ("room",)
|
||||
readonly_fields = (
|
||||
"id",
|
||||
"created_at",
|
||||
"options",
|
||||
"mode",
|
||||
"room",
|
||||
"status",
|
||||
"updated_at",
|
||||
"worker_id",
|
||||
)
|
||||
actions = [resend_notification]
|
||||
list_display = ("id", "status", "room", "get_owner", "created_at", "worker_id")
|
||||
list_filter = ["status", "room", "created_at"]
|
||||
readonly_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def get_queryset(self, request):
|
||||
"""Optimize queries by prefetching related access and user data to avoid N+1 queries."""
|
||||
|
||||
@@ -159,8 +159,6 @@ class RoomSerializer(serializers.ModelSerializer):
|
||||
configuration=configuration,
|
||||
is_admin_or_owner=is_admin_or_owner,
|
||||
)
|
||||
else:
|
||||
del output["pin_code"]
|
||||
|
||||
output["is_administrable"] = is_admin_or_owner
|
||||
|
||||
@@ -181,7 +179,6 @@ class RecordingSerializer(serializers.ModelSerializer):
|
||||
"updated_at",
|
||||
"status",
|
||||
"mode",
|
||||
"options",
|
||||
"key",
|
||||
"is_expired",
|
||||
"expired_at",
|
||||
@@ -213,11 +210,6 @@ class StartRecordingSerializer(BaseValidationOnlySerializer):
|
||||
"screen_recording or transcript.",
|
||||
},
|
||||
)
|
||||
options = serializers.JSONField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
default=dict,
|
||||
)
|
||||
|
||||
|
||||
class RequestEntrySerializer(BaseValidationOnlySerializer):
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"""Throttling modules for the API."""
|
||||
|
||||
from lasuite.drf.throttling import MonitoredThrottleMixin
|
||||
from rest_framework.throttling import AnonRateThrottle
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
|
||||
def sentry_monitoring_throttle_failure(message):
|
||||
"""Log when a failure occurs to detect rate limiting issues."""
|
||||
capture_message(message, "warning")
|
||||
|
||||
|
||||
class MonitoredAnonRateThrottle(MonitoredThrottleMixin, AnonRateThrottle):
|
||||
"""Throttle for the monitored scoped rate throttle."""
|
||||
|
||||
|
||||
class RequestEntryAnonRateThrottle(MonitoredAnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room entry"""
|
||||
|
||||
scope = "request_entry"
|
||||
|
||||
|
||||
class CreationCallbackAnonRateThrottle(MonitoredAnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room generation callback"""
|
||||
|
||||
scope = "creation_callback"
|
||||
@@ -10,7 +10,7 @@ from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.text import slugify
|
||||
|
||||
from rest_framework import decorators, mixins, pagination, viewsets
|
||||
from rest_framework import decorators, mixins, pagination, throttling, viewsets
|
||||
from rest_framework import (
|
||||
exceptions as drf_exceptions,
|
||||
)
|
||||
@@ -58,7 +58,7 @@ from core.services.room_creation import RoomCreation
|
||||
from core.services.subtitle import SubtitleException, SubtitleService
|
||||
|
||||
from ..authentication.livekit import LiveKitTokenAuthentication
|
||||
from . import permissions, serializers, throttling
|
||||
from . import permissions, serializers
|
||||
from .feature_flag import FeatureFlag
|
||||
|
||||
# pylint: disable=too-many-ancestors
|
||||
@@ -191,6 +191,18 @@ class UserViewSet(
|
||||
)
|
||||
|
||||
|
||||
class RequestEntryAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room entry"""
|
||||
|
||||
scope = "request_entry"
|
||||
|
||||
|
||||
class CreationCallbackAnonRateThrottle(throttling.AnonRateThrottle):
|
||||
"""Throttle Anonymous user requesting room generation callback"""
|
||||
|
||||
scope = "creation_callback"
|
||||
|
||||
|
||||
class RoomViewSet(
|
||||
mixins.CreateModelMixin,
|
||||
mixins.DestroyModelMixin,
|
||||
@@ -296,13 +308,10 @@ class RoomViewSet(
|
||||
)
|
||||
|
||||
mode = serializer.validated_data["mode"]
|
||||
options = serializer.validated_data["options"]
|
||||
room = self.get_object()
|
||||
|
||||
# May raise exception if an active or initiated recording already exist for the room
|
||||
recording = models.Recording.objects.create(
|
||||
room=room, mode=mode, options=options
|
||||
)
|
||||
recording = models.Recording.objects.create(room=room, mode=mode)
|
||||
|
||||
models.RecordingAccess.objects.create(
|
||||
user=self.request.user, role=models.RoleChoices.OWNER, recording=recording
|
||||
@@ -367,7 +376,7 @@ class RoomViewSet(
|
||||
methods=["post"],
|
||||
url_path="request-entry",
|
||||
permission_classes=[],
|
||||
throttle_classes=[throttling.RequestEntryAnonRateThrottle],
|
||||
throttle_classes=[RequestEntryAnonRateThrottle],
|
||||
)
|
||||
def request_entry(self, request, pk=None): # pylint: disable=unused-argument
|
||||
"""Request entry to a room"""
|
||||
@@ -477,7 +486,7 @@ class RoomViewSet(
|
||||
methods=["post"],
|
||||
url_path="creation-callback",
|
||||
permission_classes=[],
|
||||
throttle_classes=[throttling.CreationCallbackAnonRateThrottle],
|
||||
throttle_classes=[CreationCallbackAnonRateThrottle],
|
||||
)
|
||||
def creation_callback(self, request):
|
||||
"""Retrieve cached room data via an unauthenticated request with a unique ID.
|
||||
|
||||
@@ -30,7 +30,7 @@ class LiveKitTokenAuthentication(authentication.BaseAuthentication):
|
||||
raise exceptions.AuthenticationFailed("Token missing user identity")
|
||||
|
||||
try:
|
||||
user = UserModel.objects.get(sub=user_id)
|
||||
user = UserModel.objects.get(id=user_id)
|
||||
except UserModel.DoesNotExist:
|
||||
user = AnonymousUser()
|
||||
|
||||
|
||||
@@ -4,14 +4,10 @@ import logging
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
|
||||
import jwt as pyJwt
|
||||
from lasuite.oidc_resource_server.backend import ResourceServerBackend as LaSuiteBackend
|
||||
import jwt
|
||||
from rest_framework import authentication, exceptions
|
||||
|
||||
from core.models import Application
|
||||
|
||||
User = get_user_model()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -29,11 +25,9 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
Returns:
|
||||
Tuple of (user, payload) if authentication successful, None otherwise
|
||||
"""
|
||||
|
||||
auth_header = authentication.get_authorization_header(request).split()
|
||||
|
||||
if not auth_header or auth_header[0].lower() != b"bearer":
|
||||
# Defer to next authentication backend
|
||||
return None
|
||||
|
||||
if len(auth_header) != 2:
|
||||
@@ -51,8 +45,6 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate_credentials(self, token):
|
||||
"""Validate JWT token and return authenticated user.
|
||||
|
||||
If token is invalid, defer to next authentication backend.
|
||||
|
||||
Args:
|
||||
token: JWT token string
|
||||
|
||||
@@ -60,29 +52,29 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
Tuple of (user, payload)
|
||||
|
||||
Raises:
|
||||
AuthenticationFailed: If token is expired, or user not found
|
||||
AuthenticationFailed: If token is invalid, expired, or user not found
|
||||
"""
|
||||
# Decode and validate JWT
|
||||
try:
|
||||
payload = pyJwt.decode(
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithms=[settings.APPLICATION_JWT_ALG],
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
)
|
||||
except pyJwt.ExpiredSignatureError as e:
|
||||
except jwt.ExpiredSignatureError as e:
|
||||
logger.warning("Token expired")
|
||||
raise exceptions.AuthenticationFailed("Token expired.") from e
|
||||
except pyJwt.InvalidIssuerError as e:
|
||||
except jwt.InvalidIssuerError as e:
|
||||
logger.warning("Invalid JWT issuer: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except pyJwt.InvalidAudienceError as e:
|
||||
except jwt.InvalidAudienceError as e:
|
||||
logger.warning("Invalid JWT audience: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
except pyJwt.InvalidTokenError:
|
||||
# Invalid JWT token - defer to next authentication backend
|
||||
return None
|
||||
except jwt.InvalidTokenError as e:
|
||||
logger.warning("Invalid JWT token: %s", e)
|
||||
raise exceptions.AuthenticationFailed("Invalid token.") from e
|
||||
|
||||
user_id = payload.get("user_id")
|
||||
client_id = payload.get("client_id")
|
||||
@@ -96,18 +88,6 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
logger.warning("Missing 'client_id' in JWT payload")
|
||||
raise exceptions.AuthenticationFailed("Invalid token claims.")
|
||||
|
||||
try:
|
||||
application = Application.objects.get(client_id=client_id)
|
||||
except Application.DoesNotExist as e:
|
||||
logger.warning("Application not found: %s", client_id)
|
||||
raise exceptions.AuthenticationFailed("Application not found.") from e
|
||||
|
||||
if not application.active:
|
||||
logger.warning(
|
||||
"Inactive application attempted authentication: %s", client_id
|
||||
)
|
||||
raise exceptions.AuthenticationFailed("Application is disabled.")
|
||||
|
||||
if not is_delegated:
|
||||
logger.warning("Token is not marked as delegated")
|
||||
raise exceptions.AuthenticationFailed("Invalid token type.")
|
||||
@@ -127,55 +107,3 @@ class ApplicationJWTAuthentication(authentication.BaseAuthentication):
|
||||
def authenticate_header(self, request):
|
||||
"""Return authentication scheme for WWW-Authenticate header."""
|
||||
return "Bearer"
|
||||
|
||||
|
||||
class ResourceServerBackend(LaSuiteBackend):
|
||||
"""OIDC Resource Server backend for user creation and retrieval."""
|
||||
|
||||
def get_or_create_user(self, access_token, id_token, payload):
|
||||
"""Get or create user from OIDC token claims.
|
||||
|
||||
Despite the LaSuiteBackend's method name suggesting "get_or_create",
|
||||
its implementation only performs a GET operation.
|
||||
Create new user from the sub claim.
|
||||
|
||||
Args:
|
||||
access_token: The access token string
|
||||
id_token: The ID token string (unused)
|
||||
payload: Token payload dict (unused)
|
||||
|
||||
Returns:
|
||||
User instance
|
||||
|
||||
Raises:
|
||||
SuspiciousOperation: If user info validation fails
|
||||
"""
|
||||
|
||||
sub = payload.get("sub")
|
||||
|
||||
if sub is None:
|
||||
message = "User info contained no recognizable user identification"
|
||||
logger.debug(message)
|
||||
raise SuspiciousOperation(message)
|
||||
|
||||
user = self.get_user(access_token, id_token, payload)
|
||||
|
||||
if user is None and settings.OIDC_CREATE_USER:
|
||||
user = self.create_user(sub)
|
||||
|
||||
return user
|
||||
|
||||
def create_user(self, sub):
|
||||
"""Create new user from subject claim.
|
||||
|
||||
Args:
|
||||
sub: Subject identifier from token
|
||||
|
||||
Returns:
|
||||
Newly created User instance
|
||||
"""
|
||||
user = self.UserModel(sub=sub)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
|
||||
return user
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from rest_framework import exceptions, permissions
|
||||
|
||||
from .. import models
|
||||
@@ -33,11 +31,12 @@ class BaseScopePermission(permissions.BasePermission):
|
||||
Raises:
|
||||
PermissionDenied: If required scope is missing from token
|
||||
"""
|
||||
# Get the current action (e.g., 'list', 'create'), if None let DRF handle it
|
||||
# Get the current action (e.g., 'list', 'create')
|
||||
action = getattr(view, "action", None)
|
||||
if not action:
|
||||
# DRF routers return a 405 for unsupported methods
|
||||
return True
|
||||
raise exceptions.PermissionDenied(
|
||||
"Insufficient permissions. Unknown action."
|
||||
)
|
||||
|
||||
required_scope = self.scope_map.get(action)
|
||||
if not required_scope:
|
||||
@@ -56,15 +55,6 @@ class BaseScopePermission(permissions.BasePermission):
|
||||
if isinstance(token_scopes, str):
|
||||
token_scopes = token_scopes.split()
|
||||
|
||||
# Ensure scopes is a deduplicated list (preserving order) and lowercase all scopes
|
||||
token_scopes = list(dict.fromkeys(scope.lower() for scope in token_scopes))
|
||||
|
||||
if settings.OIDC_RS_SCOPES_PREFIX:
|
||||
token_scopes = [
|
||||
scope.removeprefix(f"{settings.OIDC_RS_SCOPES_PREFIX}:")
|
||||
for scope in token_scopes
|
||||
]
|
||||
|
||||
if required_scope not in token_scopes:
|
||||
raise exceptions.PermissionDenied(
|
||||
f"Insufficient permissions. Required scope: {required_scope}"
|
||||
@@ -84,23 +74,3 @@ class HasRequiredRoomScope(BaseScopePermission):
|
||||
"partial_update": models.ApplicationScope.ROOMS_UPDATE,
|
||||
"destroy": models.ApplicationScope.ROOMS_DELETE,
|
||||
}
|
||||
|
||||
|
||||
class RoomPermissions(permissions.BasePermission):
|
||||
"""Permissions applying to the room API endpoint."""
|
||||
|
||||
def has_permission(self, request, view):
|
||||
"""Allow access only to authenticated users."""
|
||||
return request.user.is_authenticated
|
||||
|
||||
def has_object_permission(self, request, view, obj):
|
||||
"""Enforce role-based access: read=any role, delete=owner, write=admin or owner."""
|
||||
user = request.user
|
||||
|
||||
if request.method in permissions.SAFE_METHODS:
|
||||
return obj.has_any_role(user)
|
||||
|
||||
if request.method == "DELETE":
|
||||
return obj.is_owner(user)
|
||||
|
||||
return obj.is_administrator_or_owner(user)
|
||||
|
||||
@@ -5,11 +5,10 @@ from logging import getLogger
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.hashers import check_password
|
||||
from django.core.exceptions import SuspiciousOperation, ValidationError
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email
|
||||
|
||||
import jwt
|
||||
from lasuite.oidc_resource_server.authentication import ResourceServerAuthentication
|
||||
from rest_framework import decorators, mixins, viewsets
|
||||
from rest_framework import (
|
||||
exceptions as drf_exceptions,
|
||||
@@ -28,7 +27,7 @@ from . import authentication, permissions, serializers
|
||||
logger = getLogger(__name__)
|
||||
|
||||
|
||||
class ApplicationViewSet(viewsets.ViewSet):
|
||||
class ApplicationViewSet(viewsets.GenericViewSet):
|
||||
"""API endpoints for application authentication and token generation."""
|
||||
|
||||
@decorators.action(
|
||||
@@ -93,39 +92,12 @@ class ApplicationViewSet(viewsets.ViewSet):
|
||||
)
|
||||
|
||||
try:
|
||||
user = models.User.objects.get(email__iexact=email)
|
||||
user = models.User.objects.get(email=email)
|
||||
except models.User.DoesNotExist as e:
|
||||
if (
|
||||
settings.APPLICATION_ALLOW_USER_CREATION
|
||||
and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION
|
||||
and not settings.OIDC_USER_SUB_FIELD_IMMUTABLE
|
||||
):
|
||||
# Create a provisional user without `sub`, identified by email only.
|
||||
#
|
||||
# This relies on Django LaSuite implicitly updating the `sub` field on the
|
||||
# user's first successful OIDC authentication. If this stops working,
|
||||
# check for behavior changes in Django LaSuite.
|
||||
#
|
||||
# `OIDC_USER_SUB_FIELD_IMMUTABLE` comes from Django LaSuite and prevents `sub`
|
||||
# updates. We override its default value to allow setting `sub` for
|
||||
# provisional users.
|
||||
user = models.User(
|
||||
sub=None,
|
||||
email=email,
|
||||
)
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
logger.info(
|
||||
"Provisional user created via application: user_id=%s, email=%s, client_id=%s",
|
||||
user.id,
|
||||
email,
|
||||
application.client_id,
|
||||
)
|
||||
else:
|
||||
raise drf_exceptions.NotFound("User not found.") from e
|
||||
except models.User.MultipleObjectsReturned as e:
|
||||
raise SuspiciousOperation(
|
||||
"Multiple user accounts share a common email."
|
||||
raise drf_exceptions.NotFound(
|
||||
{
|
||||
"error": "User not found.",
|
||||
}
|
||||
) from e
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -177,14 +149,9 @@ class RoomViewSet(
|
||||
- create: Create a new room owned by the user (requires 'rooms:create' scope)
|
||||
"""
|
||||
|
||||
authentication_classes = [
|
||||
authentication.ApplicationJWTAuthentication,
|
||||
ResourceServerAuthentication,
|
||||
]
|
||||
authentication_classes = [authentication.ApplicationJWTAuthentication]
|
||||
permission_classes = [
|
||||
api.permissions.IsAuthenticated
|
||||
& permissions.HasRequiredRoomScope
|
||||
& permissions.RoomPermissions
|
||||
api.permissions.IsAuthenticated & permissions.HasRequiredRoomScope
|
||||
]
|
||||
queryset = models.Room.objects.all()
|
||||
serializer_class = serializers.RoomSerializer
|
||||
|
||||
@@ -41,7 +41,7 @@ class Migration(migrations.Migration):
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created on')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated on')),
|
||||
('sub', models.CharField(blank=True, help_text='Optional for pending users; required upon account activation. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')),
|
||||
('sub', models.CharField(blank=True, help_text='Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only.', max_length=255, null=True, unique=True, validators=[django.core.validators.RegexValidator(message='Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/_ characters.', regex='^[\\w.@+-]+\\Z')], verbose_name='sub')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='identity email address')),
|
||||
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
|
||||
('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.2.9 on 2025-12-29 15:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0015_application_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='recording',
|
||||
name='options',
|
||||
field=models.JSONField(blank=True, default=dict, help_text='Recording options', verbose_name='Recording options'),
|
||||
),
|
||||
]
|
||||
@@ -146,8 +146,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
sub = models.CharField(
|
||||
_("sub"),
|
||||
help_text=_(
|
||||
"Optional for pending users; required upon account activation. "
|
||||
"255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
),
|
||||
max_length=255,
|
||||
unique=True,
|
||||
@@ -292,10 +291,6 @@ class Resource(BaseModel):
|
||||
role = RoleChoices.MEMBER
|
||||
return role
|
||||
|
||||
def has_any_role(self, user):
|
||||
"""Check if a user has any role on the resource."""
|
||||
return self.get_role(user) is not None
|
||||
|
||||
def is_administrator_or_owner(self, user):
|
||||
"""
|
||||
Check if a user is administrator or owner of the resource."""
|
||||
@@ -581,12 +576,6 @@ class Recording(BaseModel):
|
||||
verbose_name=_("Recording mode"),
|
||||
help_text=_("Defines the mode of recording being called."),
|
||||
)
|
||||
options = models.JSONField(
|
||||
blank=True,
|
||||
default=dict,
|
||||
verbose_name=_("Recording options"),
|
||||
help_text=_("Recording options"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "meet_recording"
|
||||
|
||||
@@ -16,23 +16,6 @@ from core import models
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def get_recording_download_base_url() -> str:
|
||||
"""Get the recording download base URL with backward compatibility."""
|
||||
new_setting = settings.RECORDING_DOWNLOAD_BASE_URL
|
||||
old_setting = settings.SCREEN_RECORDING_BASE_URL
|
||||
|
||||
if old_setting:
|
||||
logger.warning(
|
||||
"SCREEN_RECORDING_BASE_URL is deprecated and will be removed in a future version. "
|
||||
"Please use RECORDING_DOWNLOAD_BASE_URL instead."
|
||||
)
|
||||
|
||||
if new_setting:
|
||||
return new_setting
|
||||
|
||||
return old_setting
|
||||
|
||||
|
||||
class NotificationService:
|
||||
"""Service for processing recordings and notifying external services."""
|
||||
|
||||
@@ -43,12 +26,7 @@ class NotificationService:
|
||||
return self._notify_summary_service(recording)
|
||||
|
||||
if recording.mode == models.RecordingModeChoices.SCREEN_RECORDING:
|
||||
summary_success = True
|
||||
if recording.options.get("transcribe", False):
|
||||
summary_success = self._notify_summary_service(recording)
|
||||
|
||||
email_success = self._notify_user_by_email(recording)
|
||||
return email_success and summary_success
|
||||
return self._notify_user_by_email(recording)
|
||||
|
||||
logger.error(
|
||||
"Unknown recording mode %s for recording %s",
|
||||
@@ -86,7 +64,7 @@ class NotificationService:
|
||||
"domain": settings.EMAIL_DOMAIN,
|
||||
"room_name": recording.room.name,
|
||||
"recording_expiration_days": settings.RECORDING_EXPIRATION_DAYS,
|
||||
"link": f"{get_recording_download_base_url()}/{recording.id}",
|
||||
"link": f"{settings.SCREEN_RECORDING_BASE_URL}/{recording.id}",
|
||||
}
|
||||
|
||||
has_failures = False
|
||||
@@ -159,14 +137,12 @@ class NotificationService:
|
||||
"email": owner_access.user.email,
|
||||
"sub": owner_access.user.sub,
|
||||
"room": recording.room.name,
|
||||
"language": recording.options.get("language"),
|
||||
"recording_date": recording.created_at.astimezone(
|
||||
owner_access.user.timezone
|
||||
).strftime("%Y-%m-%d"),
|
||||
"recording_time": recording.created_at.astimezone(
|
||||
owner_access.user.timezone
|
||||
).strftime("%H:%M"),
|
||||
"download_link": f"{get_recording_download_base_url()}/{recording.id}",
|
||||
}
|
||||
|
||||
headers = {
|
||||
@@ -182,9 +158,9 @@ class NotificationService:
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as exc:
|
||||
except requests.HTTPError as exc:
|
||||
logger.exception(
|
||||
"Summary service error for recording %s. URL: %s. Exception: %s",
|
||||
"Summary service HTTP error for recording %s. URL: %s. Exception: %s",
|
||||
recording.id,
|
||||
settings.SUMMARY_SERVICE_ENDPOINT,
|
||||
exc,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
"""Recording-related LiveKit Events Service"""
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
from logging import getLogger
|
||||
|
||||
from livekit import api
|
||||
|
||||
from core import models, utils
|
||||
|
||||
logger = getLogger(__name__)
|
||||
@@ -18,27 +14,6 @@ class RecordingEventsError(Exception):
|
||||
class RecordingEventsService:
|
||||
"""Handles recording-related Livekit webhook events."""
|
||||
|
||||
@staticmethod
|
||||
def handle_update(recording, egress_status):
|
||||
"""Handle egress status updates and sync recording state to room metadata."""
|
||||
|
||||
room_name = str(recording.room.id)
|
||||
|
||||
status_mapping = {
|
||||
api.EgressStatus.EGRESS_ACTIVE: "started",
|
||||
api.EgressStatus.EGRESS_ENDING: "saving",
|
||||
api.EgressStatus.EGRESS_ABORTED: "aborted",
|
||||
}
|
||||
|
||||
recording_status = status_mapping.get(egress_status)
|
||||
if recording_status:
|
||||
try:
|
||||
utils.update_room_metadata(
|
||||
room_name, {"recording_status": recording_status}
|
||||
)
|
||||
except utils.MetadataUpdateException as e:
|
||||
logger.exception("Failed to update room's metadata: %s", e)
|
||||
|
||||
@staticmethod
|
||||
def handle_limit_reached(recording):
|
||||
"""Stop recording and notify participants when limit is reached."""
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import logging
|
||||
|
||||
from core import utils
|
||||
from core.models import Recording, RecordingStatusChoices
|
||||
|
||||
from .exceptions import (
|
||||
@@ -61,15 +60,6 @@ class WorkerServiceMediator:
|
||||
finally:
|
||||
recording.save()
|
||||
|
||||
mode = recording.options.get("original_mode", None) or recording.mode
|
||||
|
||||
try:
|
||||
utils.update_room_metadata(
|
||||
room_name, {"recording_mode": mode, "recording_status": "starting"}
|
||||
)
|
||||
except utils.MetadataUpdateException as e:
|
||||
logger.exception("Failed to update room's metadata: %s", e)
|
||||
|
||||
logger.info(
|
||||
"Worker started for room %s (worker ID: %s)",
|
||||
recording.room,
|
||||
|
||||
@@ -11,7 +11,7 @@ from django.conf import settings
|
||||
|
||||
from livekit import api
|
||||
|
||||
from core import models, utils
|
||||
from core import models
|
||||
from core.recording.services.recording_events import (
|
||||
RecordingEventsError,
|
||||
RecordingEventsService,
|
||||
@@ -118,10 +118,8 @@ class LiveKitEventsService:
|
||||
except Exception as e:
|
||||
raise InvalidPayloadError("Invalid webhook payload") from e
|
||||
|
||||
room_name = data.room.name or data.egress_info.room_name
|
||||
|
||||
if self._filter_regex and not self._filter_regex.search(room_name):
|
||||
logger.info("Filtered webhook event for room '%s'", room_name)
|
||||
if self._filter_regex and not self._filter_regex.search(data.room.name):
|
||||
logger.info("Filtered webhook event for room '%s'", data.room.name)
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -140,20 +138,6 @@ class LiveKitEventsService:
|
||||
# pylint: disable=not-callable
|
||||
handler(data)
|
||||
|
||||
def _handle_egress_updated(self, data):
|
||||
"""Handle 'egress_updated' event."""
|
||||
|
||||
egress_id = data.egress_info.egress_id
|
||||
try:
|
||||
recording = models.Recording.objects.get(worker_id=egress_id)
|
||||
except models.Recording.DoesNotExist as err:
|
||||
raise ActionFailedError(
|
||||
f"Recording with worker ID {egress_id} does not exist"
|
||||
) from err
|
||||
|
||||
egress_status = data.egress_info.status
|
||||
self.recording_events.handle_update(recording, egress_status)
|
||||
|
||||
def _handle_egress_ended(self, data):
|
||||
"""Handle 'egress_ended' event."""
|
||||
|
||||
@@ -166,14 +150,6 @@ class LiveKitEventsService:
|
||||
f"Recording with worker ID {data.egress_info.egress_id} does not exist"
|
||||
) from err
|
||||
|
||||
try:
|
||||
room_name = str(recording.room.id)
|
||||
utils.update_room_metadata(
|
||||
room_name, {}, ["recording_mode", "recording_status"]
|
||||
)
|
||||
except utils.MetadataUpdateException as e:
|
||||
logger.exception("Failed to update room's metadata: %s", e)
|
||||
|
||||
if (
|
||||
data.egress_info.status == api.EgressStatus.EGRESS_LIMIT_REACHED
|
||||
and recording.status == models.RecordingStatusChoices.ACTIVE
|
||||
|
||||
@@ -345,9 +345,7 @@ def test_authentication_getter_existing_user_change_fields(
|
||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||
|
||||
# One and only one additional update query when a field has changed
|
||||
# Note: .save() triggers uniqueness validation queries for unique fields,
|
||||
# adding extra SELECT queries before the UPDATE (e.g., checking unique=True on 'sub')
|
||||
with django_assert_num_queries(3):
|
||||
with django_assert_num_queries(2):
|
||||
authenticated_user = klass.get_or_create_user(
|
||||
access_token="test-token", id_token=None, payload=None
|
||||
)
|
||||
|
||||
@@ -60,26 +60,6 @@ def test_notify_external_services_screen_recording_mode(mock_notify_email):
|
||||
mock_notify_email.assert_called_once_with(recording)
|
||||
|
||||
|
||||
@mock.patch.object(NotificationService, "_notify_summary_service", return_value=True)
|
||||
@mock.patch.object(NotificationService, "_notify_user_by_email", return_value=True)
|
||||
def test_notify_external_services_screen_recording_mode_with_transcribe(
|
||||
mock_notify_email, mock_notify_summary
|
||||
):
|
||||
"""Test notification routing for screen recording mode with transcribe option."""
|
||||
|
||||
service = NotificationService()
|
||||
|
||||
recording = factories.RecordingFactory(
|
||||
mode=models.RecordingModeChoices.SCREEN_RECORDING, options={"transcribe": True}
|
||||
)
|
||||
|
||||
result = service.notify_external_services(recording)
|
||||
|
||||
assert result is True
|
||||
mock_notify_email.assert_called_once_with(recording)
|
||||
mock_notify_summary.assert_called_once_with(recording)
|
||||
|
||||
|
||||
def test_notify_external_services_unknown_mode(caplog):
|
||||
"""Test notification for unknown recording mode."""
|
||||
recording = factories.RecordingFactory()
|
||||
@@ -102,7 +82,6 @@ def test_notify_user_by_email_success(mocked_current_site, settings):
|
||||
settings.EMAIL_SUPPORT_EMAIL = "support@acme.com"
|
||||
settings.EMAIL_LOGO_IMG = "https://acme.com/logo"
|
||||
settings.SCREEN_RECORDING_BASE_URL = "https://acme.com/recordings"
|
||||
settings.RECORDING_DOWNLOAD_BASE_URL = None
|
||||
settings.EMAIL_FROM = "notifications@acme.com"
|
||||
|
||||
recording = factories.RecordingFactory(room__name="Conference Room A")
|
||||
|
||||
@@ -82,7 +82,6 @@ def test_api_recordings_list_authenticated_direct(role, settings):
|
||||
"key": recording.key,
|
||||
"created_at": recording.created_at.isoformat().replace("+00:00", "Z"),
|
||||
"mode": recording.mode,
|
||||
"options": {},
|
||||
"room": {
|
||||
"access_level": str(room.access_level),
|
||||
"id": str(room.id),
|
||||
|
||||
@@ -95,7 +95,6 @@ def test_api_recording_retrieve_administrators(settings):
|
||||
"updated_at": recording.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"options": {},
|
||||
"expired_at": None,
|
||||
"is_expired": False,
|
||||
}
|
||||
@@ -131,7 +130,6 @@ def test_api_recording_retrieve_owners(settings):
|
||||
"updated_at": recording.updated_at.isoformat().replace("+00:00", "Z"),
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"options": {},
|
||||
"expired_at": None,
|
||||
"is_expired": False,
|
||||
}
|
||||
@@ -171,7 +169,6 @@ def test_api_recording_retrieve_compute_expiration_date_correctly(settings):
|
||||
"updated_at": "2023-01-15T12:00:00Z",
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"options": {},
|
||||
"expired_at": "2023-01-16T12:00:00Z",
|
||||
"is_expired": False, # Ensure the recording is still valid and hasn't expired
|
||||
}
|
||||
@@ -212,7 +209,6 @@ def test_api_recording_retrieve_expired(settings):
|
||||
"updated_at": "2023-01-15T12:00:00Z",
|
||||
"status": str(recording.status),
|
||||
"mode": str(recording.mode),
|
||||
"options": {},
|
||||
"expired_at": "2023-01-17T12:00:00Z",
|
||||
"is_expired": True, # Ensure the recording has expired
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
# pylint: disable=redefined-outer-name,unused-argument
|
||||
|
||||
from unittest import mock
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
@@ -34,18 +33,14 @@ def mediator(mock_worker_service):
|
||||
return WorkerServiceMediator(mock_worker_service)
|
||||
|
||||
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_start_recording_success(
|
||||
mock_update_room_metadata, mediator, mock_worker_service
|
||||
):
|
||||
def test_start_recording_success(mediator, mock_worker_service):
|
||||
"""Test successful recording start"""
|
||||
# Setup
|
||||
worker_id = "test-worker-123"
|
||||
mock_worker_service.start.return_value = worker_id
|
||||
|
||||
mock_recording = RecordingFactory(
|
||||
status=RecordingStatusChoices.INITIATED,
|
||||
worker_id=None,
|
||||
status=RecordingStatusChoices.INITIATED, worker_id=None
|
||||
)
|
||||
mediator.start(mock_recording)
|
||||
|
||||
@@ -60,18 +55,12 @@ def test_start_recording_success(
|
||||
assert mock_recording.worker_id == worker_id
|
||||
assert mock_recording.status == RecordingStatusChoices.ACTIVE
|
||||
|
||||
mock_update_room_metadata.assert_called_once_with(
|
||||
str(mock_recording.room.id),
|
||||
{"recording_mode": mock_recording.mode, "recording_status": "starting"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"error_class", [WorkerRequestError, WorkerConnectionError, WorkerResponseError]
|
||||
)
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_mediator_start_recording_worker_errors(
|
||||
mock_update_room_metadata, mediator, mock_worker_service, error_class
|
||||
mediator, mock_worker_service, error_class
|
||||
):
|
||||
"""Test handling of various worker errors during start"""
|
||||
# Setup
|
||||
@@ -89,8 +78,6 @@ def test_mediator_start_recording_worker_errors(
|
||||
assert mock_recording.status == RecordingStatusChoices.FAILED_TO_START
|
||||
assert mock_recording.worker_id is None
|
||||
|
||||
mock_update_room_metadata.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"status",
|
||||
@@ -103,9 +90,8 @@ def test_mediator_start_recording_worker_errors(
|
||||
RecordingStatusChoices.ABORTED,
|
||||
],
|
||||
)
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_mediator_start_recording_from_forbidden_status(
|
||||
mock_update_room_metadata, mediator, mock_worker_service, status
|
||||
mediator, mock_worker_service, status
|
||||
):
|
||||
"""Test handling of various worker errors during start"""
|
||||
# Setup
|
||||
@@ -119,8 +105,6 @@ def test_mediator_start_recording_from_forbidden_status(
|
||||
mock_recording.refresh_from_db()
|
||||
assert mock_recording.status == status
|
||||
|
||||
mock_update_room_metadata.assert_not_called()
|
||||
|
||||
|
||||
def test_mediator_stop_recording_success(mediator, mock_worker_service):
|
||||
"""Test successful recording stop"""
|
||||
|
||||
@@ -32,6 +32,7 @@ def test_api_rooms_retrieve_anonymous_private_pk():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ def test_api_rooms_retrieve_anonymous_trusted_pk():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -69,6 +71,7 @@ def test_api_rooms_retrieve_anonymous_private_pk_no_dashes():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -85,6 +88,7 @@ def test_api_rooms_retrieve_anonymous_private_slug():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -101,6 +105,7 @@ def test_api_rooms_retrieve_anonymous_private_slug_not_normalized():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
@@ -342,6 +347,7 @@ def test_api_rooms_retrieve_authenticated():
|
||||
"id": str(room.id),
|
||||
"is_administrable": False,
|
||||
"name": room.name,
|
||||
"pin_code": room.pin_code,
|
||||
"slug": room.slug,
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ from core.services.livekit_events import (
|
||||
)
|
||||
from core.services.lobby import LobbyService
|
||||
from core.services.telephony import TelephonyException, TelephonyService
|
||||
from core.utils import MetadataUpdateException, NotificationError
|
||||
from core.utils import NotificationError
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -70,10 +70,7 @@ def test_initialization(
|
||||
),
|
||||
)
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_success(
|
||||
mock_update_room_metadata, mock_notify, mode, notification_type, service
|
||||
):
|
||||
def test_handle_egress_ended_success(mock_notify, mode, notification_type, service):
|
||||
"""Should successfully stop recording and notifies all participant."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", mode=mode, status="active")
|
||||
@@ -86,98 +83,13 @@ def test_handle_egress_ended_success(
|
||||
mock_notify.assert_called_once_with(
|
||||
room_name=str(recording.room.id), notification_data={"type": notification_type}
|
||||
)
|
||||
mock_update_room_metadata.assert_called_once_with(
|
||||
str(recording.room.id), {}, ["recording_mode", "recording_status"]
|
||||
)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "stopped"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("egress_status", "status"),
|
||||
(
|
||||
(EgressStatus.EGRESS_ACTIVE, "started"),
|
||||
(EgressStatus.EGRESS_ENDING, "saving"),
|
||||
(EgressStatus.EGRESS_ABORTED, "aborted"),
|
||||
),
|
||||
)
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_updated_success(
|
||||
mock_update_room_metadata, egress_status, status, service
|
||||
):
|
||||
"""Should successfully update room's metadata."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="initiated")
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = egress_status
|
||||
|
||||
service._handle_egress_updated(mock_data)
|
||||
|
||||
mock_update_room_metadata.assert_called_once_with(
|
||||
str(recording.room.id), {"recording_status": status}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"egress_status",
|
||||
(
|
||||
EgressStatus.EGRESS_FAILED,
|
||||
EgressStatus.EGRESS_LIMIT_REACHED,
|
||||
),
|
||||
)
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_updated_non_handled(
|
||||
mock_update_room_metadata, egress_status, service
|
||||
):
|
||||
"""Should ignore certain egress status and don't trigger metadata updates."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="initiated")
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = egress_status
|
||||
|
||||
service._handle_egress_updated(mock_data)
|
||||
|
||||
mock_update_room_metadata.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("mode", "notification_type"),
|
||||
(
|
||||
("screen_recording", "screenRecordingLimitReached"),
|
||||
("transcript", "transcriptionLimitReached"),
|
||||
),
|
||||
)
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_metadata_update_fails(
|
||||
mock_update_room_metadata, mock_notify, mode, notification_type, service
|
||||
):
|
||||
"""Should successfully stop recording when metadata's update fails."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", mode=mode, status="active")
|
||||
mock_data = mock.MagicMock()
|
||||
mock_data.egress_info.egress_id = recording.worker_id
|
||||
mock_data.egress_info.status = EgressStatus.EGRESS_LIMIT_REACHED
|
||||
|
||||
mock_update_room_metadata.side_effect = MetadataUpdateException("Error notifying")
|
||||
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
mock_notify.assert_called_once_with(
|
||||
room_name=str(recording.room.id), notification_data={"type": notification_type}
|
||||
)
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "stopped"
|
||||
|
||||
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_notification_fails(
|
||||
mock_update_room_metadata, mock_notify, service
|
||||
):
|
||||
def test_handle_egress_ended_notification_fails(mock_notify, service):
|
||||
"""Should raise ActionFailedError when notification fails but still stop recording."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="active")
|
||||
@@ -196,16 +108,9 @@ def test_handle_egress_ended_notification_fails(
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "stopped"
|
||||
|
||||
mock_update_room_metadata.assert_called_once_with(
|
||||
str(recording.room.id), {}, ["recording_mode", "recording_status"]
|
||||
)
|
||||
|
||||
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_recording_not_found(
|
||||
mock_update_room_metadata, mock_notify, service
|
||||
):
|
||||
def test_handle_egress_ended_recording_not_found(mock_notify, service):
|
||||
"""Should raise ActionFailedError when recording doesn't exist."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="active")
|
||||
@@ -219,17 +124,13 @@ def test_handle_egress_ended_recording_not_found(
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
mock_notify.assert_not_called()
|
||||
mock_update_room_metadata.assert_not_called()
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "active"
|
||||
|
||||
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_recording_not_active(
|
||||
mock_update_room_metadata, mock_notify, service
|
||||
):
|
||||
def test_handle_egress_ended_recording_not_active(mock_notify, service):
|
||||
"""Should ignore non-active recordings."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="failed_to_stop")
|
||||
@@ -240,19 +141,13 @@ def test_handle_egress_ended_recording_not_active(
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
mock_notify.assert_not_called()
|
||||
mock_update_room_metadata.assert_called_once_with(
|
||||
str(recording.room.id), {}, ["recording_mode", "recording_status"]
|
||||
)
|
||||
|
||||
recording.refresh_from_db()
|
||||
assert recording.status == "failed_to_stop"
|
||||
|
||||
|
||||
@mock.patch("core.utils.notify_participants")
|
||||
@mock.patch("core.utils.update_room_metadata")
|
||||
def test_handle_egress_ended_recording_not_limit_reached(
|
||||
mock_update_room_metadata, mock_notify, service
|
||||
):
|
||||
def test_handle_egress_ended_recording_not_limit_reached(mock_notify, service):
|
||||
"""Should ignore egress non-limit-reached statuses."""
|
||||
|
||||
recording = RecordingFactory(worker_id="worker-1", status="stopped")
|
||||
@@ -263,9 +158,6 @@ def test_handle_egress_ended_recording_not_limit_reached(
|
||||
service._handle_egress_ended(mock_data)
|
||||
|
||||
mock_notify.assert_not_called()
|
||||
mock_update_room_metadata.assert_called_once_with(
|
||||
str(recording.room.id), {}, ["recording_mode", "recording_status"]
|
||||
)
|
||||
assert recording.status == "stopped"
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@ from core.factories import (
|
||||
ApplicationFactory,
|
||||
UserFactory,
|
||||
)
|
||||
from core.models import ApplicationScope, User
|
||||
from core.models import ApplicationScope
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -22,7 +22,7 @@ pytestmark = pytest.mark.django_db
|
||||
def test_api_applications_generate_token_success(settings):
|
||||
"""Valid credentials should return a JWT token."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
UserFactory(email="User.Family@example.com")
|
||||
user = UserFactory(email="user@example.com")
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
@@ -40,7 +40,7 @@ def test_api_applications_generate_token_success(settings):
|
||||
"client_id": application.client_id,
|
||||
"client_secret": plain_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": "user.family@example.com",
|
||||
"scope": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
@@ -232,7 +232,6 @@ def test_api_applications_token_payload_structure(settings):
|
||||
"""Generated token should have correct payload structure."""
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory(email="user@example.com")
|
||||
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
@@ -274,119 +273,3 @@ def test_api_applications_token_payload_structure(settings):
|
||||
"delegated": True,
|
||||
"scope": "rooms:list rooms:create",
|
||||
}
|
||||
|
||||
|
||||
@freeze_time("2023-01-15 12:00:00")
|
||||
def test_api_applications_token_new_user(settings):
|
||||
"""Should create a new pending user when creation is allowed and user doesn't exist."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
settings.APPLICATION_ALLOW_USER_CREATION = True
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
settings.OIDC_USER_SUB_FIELD_IMMUTABLE = False
|
||||
|
||||
assert len(User.objects.all()) == 0
|
||||
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
)
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
application.save()
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": application.client_id,
|
||||
"client_secret": plain_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": "unknown@world.com",
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Decode token to verify payload
|
||||
token = response.data["access_token"]
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithms=[settings.APPLICATION_JWT_ALG],
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
)
|
||||
|
||||
user = User.objects.get(email="unknown@world.com")
|
||||
assert user.sub is None
|
||||
|
||||
assert payload == {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"client_id": application.client_id,
|
||||
"exp": 1673787600,
|
||||
"iat": 1673784000,
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
"scope": "rooms:list rooms:create",
|
||||
}
|
||||
|
||||
|
||||
@freeze_time("2023-01-15 12:00:00")
|
||||
def test_api_applications_token_existing_user(settings):
|
||||
"""Application should not create a new user when user exist."""
|
||||
|
||||
settings.APPLICATION_JWT_SECRET_KEY = "devKey"
|
||||
user = UserFactory(email="user@example.com")
|
||||
|
||||
settings.APPLICATION_ALLOW_USER_CREATION = True
|
||||
settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = True
|
||||
settings.OIDC_USER_SUB_FIELD_IMMUTABLE = False
|
||||
|
||||
assert len(User.objects.all()) == 1
|
||||
|
||||
application = ApplicationFactory(
|
||||
active=True,
|
||||
scopes=[ApplicationScope.ROOMS_LIST, ApplicationScope.ROOMS_CREATE],
|
||||
)
|
||||
|
||||
plain_secret = "test-secret-123"
|
||||
application.client_secret = plain_secret
|
||||
application.save()
|
||||
|
||||
client = APIClient()
|
||||
response = client.post(
|
||||
"/external-api/v1.0/application/token/",
|
||||
{
|
||||
"client_id": application.client_id,
|
||||
"client_secret": plain_secret,
|
||||
"grant_type": "client_credentials",
|
||||
"scope": user.email,
|
||||
},
|
||||
format="json",
|
||||
)
|
||||
|
||||
# Assert no new user was created
|
||||
assert len(User.objects.all()) == 1
|
||||
|
||||
# Decode token to verify payload
|
||||
token = response.data["access_token"]
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.APPLICATION_JWT_SECRET_KEY,
|
||||
algorithms=[settings.APPLICATION_JWT_ALG],
|
||||
issuer=settings.APPLICATION_JWT_ISSUER,
|
||||
audience=settings.APPLICATION_JWT_AUDIENCE,
|
||||
)
|
||||
|
||||
assert payload == {
|
||||
"iss": settings.APPLICATION_JWT_ISSUER,
|
||||
"aud": settings.APPLICATION_JWT_AUDIENCE,
|
||||
"client_id": application.client_id,
|
||||
"exp": 1673787600,
|
||||
"iat": 1673784000,
|
||||
"user_id": str(user.id),
|
||||
"delegated": True,
|
||||
"scope": "rooms:list rooms:create",
|
||||
}
|
||||
|
||||
@@ -25,7 +25,6 @@ from livekit.api import ( # pylint: disable=E0611
|
||||
LiveKitAPI,
|
||||
SendDataRequest,
|
||||
TwirpError,
|
||||
UpdateRoomMetadataRequest,
|
||||
VideoGrants,
|
||||
)
|
||||
|
||||
@@ -245,57 +244,6 @@ async def notify_participants(room_name: str, notification_data: dict):
|
||||
await lkapi.aclose()
|
||||
|
||||
|
||||
class MetadataUpdateException(Exception):
|
||||
"""Room's metadata update fails."""
|
||||
|
||||
|
||||
@async_to_sync
|
||||
async def update_room_metadata(
|
||||
room_name: str, metadata: dict, remove_keys: Optional[list[str]] = None
|
||||
):
|
||||
"""Update LiveKit room metadata by merging new values with existing metadata.
|
||||
|
||||
Args:
|
||||
room_name: Name of the room to update
|
||||
metadata: Dictionary of metadata key-values to add/update
|
||||
remove_keys: Optional list of keys to remove from existing metadata.
|
||||
"""
|
||||
|
||||
lkapi = create_livekit_client()
|
||||
|
||||
try:
|
||||
response = await lkapi.room.list_rooms(
|
||||
ListRoomsRequest(
|
||||
names=[room_name],
|
||||
)
|
||||
)
|
||||
|
||||
if not response.rooms:
|
||||
return
|
||||
|
||||
room = response.rooms[0]
|
||||
|
||||
existing_metadata = json.loads(room.metadata) if room.metadata else {}
|
||||
|
||||
if remove_keys:
|
||||
for key in remove_keys:
|
||||
existing_metadata.pop(key, None)
|
||||
|
||||
updated_metadata = {**existing_metadata, **metadata}
|
||||
|
||||
await lkapi.room.update_room_metadata(
|
||||
UpdateRoomMetadataRequest(
|
||||
room=room_name, metadata=json.dumps(updated_metadata).encode("utf-8")
|
||||
)
|
||||
)
|
||||
except TwirpError as e:
|
||||
raise MetadataUpdateException(
|
||||
f"Failed to update metadata for room {room_name}: {e}"
|
||||
) from e
|
||||
finally:
|
||||
await lkapi.aclose()
|
||||
|
||||
|
||||
ALPHANUMERIC_CHARSET = string.ascii_letters + string.digits
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,153 +17,121 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:29
|
||||
#: core/admin.py:26
|
||||
msgid "Personal info"
|
||||
msgstr "Persönliche Informationen"
|
||||
|
||||
#: core/admin.py:42
|
||||
#: core/admin.py:39
|
||||
msgid "Permissions"
|
||||
msgstr "Berechtigungen"
|
||||
|
||||
#: core/admin.py:54
|
||||
#: core/admin.py:51
|
||||
msgid "Important dates"
|
||||
msgstr "Wichtige Daten"
|
||||
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
#: core/admin.py:147
|
||||
msgid "No owner"
|
||||
msgstr "Kein Eigentümer"
|
||||
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
#: core/admin.py:150
|
||||
msgid "Multiple owners"
|
||||
msgstr "Mehrere Eigentümer"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Benachrichtigung erneut an externen Dienst senden"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Benachrichtigung für Aufnahme %(id)s fehlgeschlagen"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Benachrichtigung für Aufnahme %(id)s fehlgeschlagen: %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Benachrichtigungen für %(count)s Aufnahme(n) erfolgreich gesendet."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s abgelaufene Aufnahme(n) übersprungen."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "Keine Scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
#: core/api/serializers.py:67
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Sie müssen Administrator oder Eigentümer eines Raums sein, um Zugriffe "
|
||||
"hinzuzufügen."
|
||||
|
||||
#: core/models.py:34
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Mitglied"
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Eigentümer"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Gestartet"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Aktiv"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Beendet"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Gespeichert"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Abgebrochen"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Start fehlgeschlagen"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stopp fehlgeschlagen"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Benachrichtigung erfolgreich"
|
||||
|
||||
#: core/models.py:86
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "BILDSCHIRMAUFZEICHNUNG"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSKRIPT"
|
||||
|
||||
#: core/models.py:93
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Öffentlicher Zugriff"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrauenswürdiger Zugriff"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Eingeschränkter Zugriff"
|
||||
|
||||
#: core/models.py:107
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "ID"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "Primärschlüssel des Eintrags als UUID"
|
||||
|
||||
#: core/models.py:114
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "erstellt am"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "Datum und Uhrzeit der Erstellung eines Eintrags"
|
||||
|
||||
#: core/models.py:120
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "aktualisiert am"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "Datum und Uhrzeit der letzten Aktualisierung eines Eintrags"
|
||||
|
||||
#: core/models.py:141
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -171,67 +139,67 @@ msgstr ""
|
||||
"Geben Sie einen gültigen Sub ein. Dieser Wert darf nur Buchstaben, Zahlen "
|
||||
"und die Zeichen @/./+/-/_ enthalten."
|
||||
|
||||
#: core/models.py:147
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "Sub"
|
||||
|
||||
#: core/models.py:149
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Optional für ausstehende Benutzer; erforderlich nach Kontoaktivierung. "
|
||||
"Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ Zeichen erlaubt."
|
||||
"Erforderlich. Maximal 255 Zeichen. Nur Buchstaben, Zahlen und @/./+/-/_ sind "
|
||||
"erlaubt."
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "Identitäts-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "Administrator-E-Mail-Adresse"
|
||||
|
||||
#: core/models.py:165
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "Vollständiger Name"
|
||||
|
||||
#: core/models.py:167
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "Kurzname"
|
||||
|
||||
#: core/models.py:173
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "Sprache"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "Die Sprache, in der der Benutzer die Oberfläche sehen möchte."
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Die Zeitzone, in der der Benutzer die Zeiten sehen möchte."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "Gerät"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Ob es sich um ein Gerät oder einen echten Benutzer handelt."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "Mitarbeiterstatus"
|
||||
|
||||
#: core/models.py:190
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Ob der Benutzer sich bei dieser Admin-Seite anmelden kann."
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "aktiv"
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -239,66 +207,66 @@ msgstr ""
|
||||
"Ob dieser Benutzer als aktiv behandelt werden soll. Deaktivieren Sie dies "
|
||||
"anstelle des Löschens des Kontos."
|
||||
|
||||
#: core/models.py:209
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "Benutzer"
|
||||
|
||||
#: core/models.py:269
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Ressourcen"
|
||||
|
||||
#: core/models.py:324
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Ressourcenzugriff"
|
||||
|
||||
#: core/models.py:325
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Ressourcenzugriffe"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"Ein Ressourcenzugriff mit diesem Benutzer und dieser Ressource existiert "
|
||||
"bereits."
|
||||
|
||||
#: core/models.py:387
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-Raumkonfiguration"
|
||||
|
||||
#: core/models.py:388
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Werte für Visio-Parameter zur Konfiguration des Raums."
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "PIN-Code für den Raum"
|
||||
|
||||
#: core/models.py:396
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Eindeutiger n-stelliger Code, der diesen Raum im Telephonmodus identifiziert."
|
||||
|
||||
#: core/models.py:402 core/models.py:556
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Raum"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Räume"
|
||||
|
||||
#: core/models.py:567
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker-ID"
|
||||
|
||||
#: core/models.py:569
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -307,107 +275,42 @@ msgstr ""
|
||||
"erhalten, auch wenn der Worker stoppt, was ein einfaches Nachverfolgen "
|
||||
"ermöglicht."
|
||||
|
||||
#: core/models.py:577
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Aufzeichnungsmodus"
|
||||
|
||||
#: core/models.py:578
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definiert den aufgerufenen Aufzeichnungsmodus."
|
||||
|
||||
#: core/models.py:583 core/models.py:584
|
||||
msgid "Recording options"
|
||||
msgstr "Aufnahmeoptionen"
|
||||
|
||||
#: core/models.py:590
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Aufzeichnung"
|
||||
|
||||
#: core/models.py:591
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Aufzeichnungen"
|
||||
|
||||
#: core/models.py:699
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Beziehung Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:700
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Beziehungen Aufzeichnung/Benutzer"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Dieser Benutzer ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:712
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dieses Team ist bereits Teil dieser Aufzeichnung."
|
||||
|
||||
#: core/models.py:718
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Entweder Benutzer oder Team muss festgelegt werden, nicht beides."
|
||||
|
||||
#: core/models.py:735
|
||||
msgid "Create rooms"
|
||||
msgstr "Räume erstellen"
|
||||
|
||||
#: core/models.py:736
|
||||
msgid "List rooms"
|
||||
msgstr "Räume auflisten"
|
||||
|
||||
#: core/models.py:737
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Raumdetails abrufen"
|
||||
|
||||
#: core/models.py:738
|
||||
msgid "Update rooms"
|
||||
msgstr "Räume aktualisieren"
|
||||
|
||||
#: core/models.py:739
|
||||
msgid "Delete rooms"
|
||||
msgstr "Räume löschen"
|
||||
|
||||
#: core/models.py:752
|
||||
msgid "Application name"
|
||||
msgstr "Anwendungsname"
|
||||
|
||||
#: core/models.py:753
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschreibender Name für diese Anwendung."
|
||||
|
||||
#: core/models.py:763
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Beim Speichern gehasht. Jetzt kopieren, wenn dies ein neues Geheimnis ist."
|
||||
|
||||
#: core/models.py:774
|
||||
msgid "Application"
|
||||
msgstr "Anwendung"
|
||||
|
||||
#: core/models.py:775
|
||||
msgid "Applications"
|
||||
msgstr "Anwendungen"
|
||||
|
||||
#: core/models.py:798
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Geben Sie eine gültige Domain ein"
|
||||
|
||||
#: core/models.py:801
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:802
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-Mail-Domain, im Namen der diese Anwendung handeln kann."
|
||||
|
||||
#: core/models.py:814
|
||||
msgid "Application domain"
|
||||
msgstr "Anwendungsdomain"
|
||||
|
||||
#: core/models.py:815
|
||||
msgid "Application domains"
|
||||
msgstr "Anwendungsdomains"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Ihre Aufzeichnung ist bereit"
|
||||
@@ -498,8 +401,7 @@ msgid ""
|
||||
" Sharing the recording via link is not yet available. Only organizers can "
|
||||
"download it. "
|
||||
msgstr ""
|
||||
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur "
|
||||
"Organisatoren können sie herunterladen. "
|
||||
" Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisatoren können sie herunterladen. "
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:206
|
||||
#: core/templates/mail/text/screen_recording.txt:11
|
||||
@@ -536,18 +438,18 @@ msgstr ""
|
||||
" Wenn Sie Fragen haben oder Unterstützung benötigen, wenden Sie sich bitte "
|
||||
"an unser Support-Team unter %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:169
|
||||
#: meet/settings.py:163
|
||||
msgid "English"
|
||||
msgstr "Englisch"
|
||||
|
||||
#: meet/settings.py:170
|
||||
#: meet/settings.py:164
|
||||
msgid "French"
|
||||
msgstr "Französisch"
|
||||
|
||||
#: meet/settings.py:171
|
||||
#: meet/settings.py:165
|
||||
msgid "Dutch"
|
||||
msgstr "Niederländisch"
|
||||
|
||||
#: meet/settings.py:172
|
||||
#: meet/settings.py:166
|
||||
msgid "German"
|
||||
msgstr "Deutsch"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,151 +17,119 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:29
|
||||
#: core/admin.py:26
|
||||
msgid "Personal info"
|
||||
msgstr "Personal info"
|
||||
msgstr ""
|
||||
|
||||
#: core/admin.py:42
|
||||
#: core/admin.py:39
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
|
||||
#: core/admin.py:54
|
||||
#: core/admin.py:51
|
||||
msgid "Important dates"
|
||||
msgstr "Important dates"
|
||||
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
#: core/admin.py:147
|
||||
msgid "No owner"
|
||||
msgstr "No owner"
|
||||
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
#: core/admin.py:150
|
||||
msgid "Multiple owners"
|
||||
msgstr "Multiple owners"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Resend notification to external service"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Failed to notify for recording %(id)s"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Failed to notify for recording %(id)s: %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Successfully sent notifications for %(count)s recording(s)."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "Skipped %(count)s expired recording(s)."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "No scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
#: core/api/serializers.py:67
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr "You must be administrator or owner of a room to add accesses to it."
|
||||
|
||||
#: core/models.py:34
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Member"
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Administrator"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Owner"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Initiated"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Active"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Stopped"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Saved"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Aborted"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Failed to Start"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Failed to Stop"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification succeeded"
|
||||
|
||||
#: core/models.py:86
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCREEN_RECORDING"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:93
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Public Access"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Trusted Access"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Restricted Access"
|
||||
|
||||
#: core/models.py:107
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primary key for the record as UUID"
|
||||
|
||||
#: core/models.py:114
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "created on"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date and time at which a record was created"
|
||||
|
||||
#: core/models.py:120
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "updated on"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "date and time at which a record was last updated"
|
||||
|
||||
#: core/models.py:141
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -169,67 +137,67 @@ msgstr ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
|
||||
#: core/models.py:147
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:149
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "identity email address"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "admin email address"
|
||||
|
||||
#: core/models.py:165
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "full name"
|
||||
|
||||
#: core/models.py:167
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "short name"
|
||||
|
||||
#: core/models.py:173
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "language"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "The language in which the user wants to see the interface."
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "The timezone in which the user wants to see times."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "device"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Whether the user is a device or a real user."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "staff status"
|
||||
|
||||
#: core/models.py:190
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Whether the user can log into this admin site."
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "active"
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -237,63 +205,63 @@ msgstr ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
|
||||
#: core/models.py:209
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "user"
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "users"
|
||||
|
||||
#: core/models.py:269
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Resource"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Resources"
|
||||
|
||||
#: core/models.py:324
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Resource access"
|
||||
|
||||
#: core/models.py:325
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Resource accesses"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Resource access with this User and Resource already exists."
|
||||
|
||||
#: core/models.py:387
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio room configuration"
|
||||
|
||||
#: core/models.py:388
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Values for Visio parameters to configure the room."
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "Room PIN code"
|
||||
|
||||
#: core/models.py:396
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr "Unique n-digit code that identifies this room in telephony mode."
|
||||
|
||||
#: core/models.py:402 core/models.py:556
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Room"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Rooms"
|
||||
|
||||
#: core/models.py:567
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:569
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -301,110 +269,42 @@ msgstr ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
|
||||
#: core/models.py:577
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Recording mode"
|
||||
|
||||
#: core/models.py:578
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Defines the mode of recording being called."
|
||||
|
||||
#: core/models.py:583 core/models.py:584
|
||||
msgid "Recording options"
|
||||
msgstr "Recording options"
|
||||
|
||||
#: core/models.py:590
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Recording"
|
||||
|
||||
#: core/models.py:591
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Recordings"
|
||||
|
||||
#: core/models.py:699
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Recording/user relation"
|
||||
|
||||
#: core/models.py:700
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Recording/user relations"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "This user is already in this recording."
|
||||
|
||||
#: core/models.py:712
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "This team is already in this recording."
|
||||
|
||||
#: core/models.py:718
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Either user or team must be set, not both."
|
||||
|
||||
#: core/models.py:735
|
||||
#, fuzzy
|
||||
#| msgid "created on"
|
||||
msgid "Create rooms"
|
||||
msgstr "Create rooms"
|
||||
|
||||
#: core/models.py:736
|
||||
msgid "List rooms"
|
||||
msgstr "List rooms"
|
||||
|
||||
#: core/models.py:737
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Retrieve room details"
|
||||
|
||||
#: core/models.py:738
|
||||
#, fuzzy
|
||||
#| msgid "updated on"
|
||||
msgid "Update rooms"
|
||||
msgstr "Update rooms"
|
||||
|
||||
#: core/models.py:739
|
||||
msgid "Delete rooms"
|
||||
msgstr "Delete rooms"
|
||||
|
||||
#: core/models.py:752
|
||||
msgid "Application name"
|
||||
msgstr "Application name"
|
||||
|
||||
#: core/models.py:753
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Descriptive name for this application."
|
||||
|
||||
#: core/models.py:763
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr "Hashed on Save. Copy it now if this is a new secret."
|
||||
|
||||
#: core/models.py:774
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:775
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:798
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Enter a valid domain"
|
||||
|
||||
#: core/models.py:801
|
||||
msgid "Domain"
|
||||
msgstr "Domain"
|
||||
|
||||
#: core/models.py:802
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Email domain this application can act on behalf of."
|
||||
|
||||
#: core/models.py:814
|
||||
msgid "Application domain"
|
||||
msgstr "Application domain"
|
||||
|
||||
#: core/models.py:815
|
||||
msgid "Application domains"
|
||||
msgstr "Application domains"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Your recording is ready"
|
||||
@@ -533,18 +433,18 @@ msgstr ""
|
||||
" If you have any questions or need assistance, please contact our support "
|
||||
"team at %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:169
|
||||
#: meet/settings.py:163
|
||||
msgid "English"
|
||||
msgstr "English"
|
||||
|
||||
#: meet/settings.py:170
|
||||
#: meet/settings.py:164
|
||||
msgid "French"
|
||||
msgstr "French"
|
||||
|
||||
#: meet/settings.py:171
|
||||
#: meet/settings.py:165
|
||||
msgid "Dutch"
|
||||
msgstr "Dutch"
|
||||
|
||||
#: meet/settings.py:172
|
||||
#: meet/settings.py:166
|
||||
msgid "German"
|
||||
msgstr "German"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: antoine.lebaud@mail.numerique.gouv.fr\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,155 +17,123 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:29
|
||||
#: core/admin.py:26
|
||||
msgid "Personal info"
|
||||
msgstr "Informations personnelles"
|
||||
|
||||
#: core/admin.py:42
|
||||
#: core/admin.py:39
|
||||
msgid "Permissions"
|
||||
msgstr "Permissions"
|
||||
|
||||
#: core/admin.py:54
|
||||
#: core/admin.py:51
|
||||
msgid "Important dates"
|
||||
msgstr "Dates importantes"
|
||||
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
#: core/admin.py:147
|
||||
msgid "No owner"
|
||||
msgstr "Pas de propriétaire"
|
||||
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
#: core/admin.py:150
|
||||
msgid "Multiple owners"
|
||||
msgstr "Plusieurs propriétaires"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Renvoyer la notification au service externe"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Échec de la notification pour l’enregistrement %(id)s"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Échec de la notification pour l’enregistrement %(id)s : %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Notifications envoyées avec succès pour %(count)s enregistrement(s)."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s enregistrement(s) expiré(s) ignoré(s)."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "Aucun scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
#: core/api/serializers.py:67
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Vous devez être administrateur ou propriétaire d'une salle pour y ajouter "
|
||||
"des accès."
|
||||
|
||||
#: core/models.py:34
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Membre"
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Administrateur"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Propriétaire"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Initié"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Actif"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Arrêté"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Enregistré"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Abandonné"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Échec au démarrage"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Échec à l'arrêt"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notification réussie"
|
||||
|
||||
#: core/models.py:86
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "ENREGISTREMENT_ÉCRAN"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPTION"
|
||||
|
||||
#: core/models.py:93
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Accès public"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Accès de confiance"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Accès restreint"
|
||||
|
||||
#: core/models.py:107
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "clé primaire pour l'enregistrement sous forme d'UUID"
|
||||
|
||||
#: core/models.py:114
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "créé le"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "date et heure auxquelles un enregistrement a été créé"
|
||||
|
||||
#: core/models.py:120
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "mis à jour le"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr ""
|
||||
"date et heure auxquelles un enregistrement a été mis à jour pour la dernière "
|
||||
"fois"
|
||||
|
||||
#: core/models.py:141
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -173,67 +141,67 @@ msgstr ""
|
||||
"Entrez un sub valide. Cette valeur ne peut contenir que des lettres, des "
|
||||
"chiffres et les caractères @/./+/-/_."
|
||||
|
||||
#: core/models.py:147
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:149
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Optionnel pour les utilisateurs en attente ; requis lors de l'activation du "
|
||||
"compte. 255 caractères maximum. Lettres, chiffres et @/./+/-/_ uniquement."
|
||||
"Obligatoire. 255 caractères ou moins. Lettres, chiffres et caractères @/./"
|
||||
"+/-/_ uniquement."
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "adresse e-mail d'identité"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "adresse e-mail d'administrateur"
|
||||
|
||||
#: core/models.py:165
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "nom complet"
|
||||
|
||||
#: core/models.py:167
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "nom court"
|
||||
|
||||
#: core/models.py:173
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "langue"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "La langue dans laquelle l'utilisateur souhaite voir l'interface."
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "Le fuseau horaire dans lequel l'utilisateur souhaite voir les heures."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "appareil"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Si l'utilisateur est un appareil ou un utilisateur réel."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "statut du personnel"
|
||||
|
||||
#: core/models.py:190
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Si l'utilisateur peut se connecter à ce site d'administration."
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "actif"
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -241,65 +209,65 @@ msgstr ""
|
||||
"Si cet utilisateur doit être traité comme actif. Désélectionnez cette option "
|
||||
"au lieu de supprimer des comptes."
|
||||
|
||||
#: core/models.py:209
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "utilisateur"
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "utilisateurs"
|
||||
|
||||
#: core/models.py:269
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Ressource"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Ressources"
|
||||
|
||||
#: core/models.py:324
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:325
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Accès aux ressources"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr ""
|
||||
"L'accès à la ressource avec cet utilisateur et cette ressource existe déjà."
|
||||
|
||||
#: core/models.py:387
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Configuration de la salle de visioconférence"
|
||||
|
||||
#: core/models.py:388
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Valeurs des paramètres de visioconférence pour configurer la salle."
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "Code PIN de la salle"
|
||||
|
||||
#: core/models.py:396
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Code unique à n chiffres qui identifie cette salle en mode téléphonique."
|
||||
|
||||
#: core/models.py:402 core/models.py:556
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Salle"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Salles"
|
||||
|
||||
#: core/models.py:567
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "ID du Worker"
|
||||
|
||||
#: core/models.py:569
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -307,108 +275,42 @@ msgstr ""
|
||||
"Entrez un identifiant pour l'enregistrement du Worker. Cet identifiant est "
|
||||
"conservé même lorsque le Worker s'arrête, permettant un suivi facile."
|
||||
|
||||
#: core/models.py:577
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Mode d'enregistrement"
|
||||
|
||||
#: core/models.py:578
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Définit le mode d'enregistrement appelé."
|
||||
|
||||
#: core/models.py:583 core/models.py:584
|
||||
msgid "Recording options"
|
||||
msgstr "Options d'enregistrement"
|
||||
|
||||
#: core/models.py:590
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Enregistrement"
|
||||
|
||||
#: core/models.py:591
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Enregistrements"
|
||||
|
||||
#: core/models.py:699
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Relation enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:700
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Relations enregistrement/utilisateur"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Cet utilisateur est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:712
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Cette équipe est déjà dans cet enregistrement."
|
||||
|
||||
#: core/models.py:718
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Soit l'utilisateur, soit l'équipe doit être défini, pas les deux."
|
||||
|
||||
#: core/models.py:735
|
||||
msgid "Create rooms"
|
||||
msgstr "Créer des salles"
|
||||
|
||||
#: core/models.py:736
|
||||
msgid "List rooms"
|
||||
msgstr "Lister les salles"
|
||||
|
||||
#: core/models.py:737
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Afficher les détails d’une salle"
|
||||
|
||||
#: core/models.py:738
|
||||
msgid "Update rooms"
|
||||
msgstr "Mettre à jour les salles"
|
||||
|
||||
#: core/models.py:739
|
||||
msgid "Delete rooms"
|
||||
msgstr "Supprimer les salles"
|
||||
|
||||
#: core/models.py:752
|
||||
msgid "Application name"
|
||||
msgstr "Nom de l’application"
|
||||
|
||||
#: core/models.py:753
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Nom descriptif de cette application."
|
||||
|
||||
#: core/models.py:763
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Haché lors de l’enregistrement. Copiez-le maintenant s’il s’agit d’un "
|
||||
"nouveau secret."
|
||||
|
||||
#: core/models.py:774
|
||||
msgid "Application"
|
||||
msgstr "Application"
|
||||
|
||||
#: core/models.py:775
|
||||
msgid "Applications"
|
||||
msgstr "Applications"
|
||||
|
||||
#: core/models.py:798
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Saisissez un domaine valide"
|
||||
|
||||
#: core/models.py:801
|
||||
msgid "Domain"
|
||||
msgstr "Domaine"
|
||||
|
||||
#: core/models.py:802
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "Domaine de messagerie au nom duquel cette application peut agir."
|
||||
|
||||
#: core/models.py:814
|
||||
msgid "Application domain"
|
||||
msgstr "Domaine d’application"
|
||||
|
||||
#: core/models.py:815
|
||||
msgid "Application domains"
|
||||
msgstr "Domaines d’application"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Votre enregistrement est prêt"
|
||||
@@ -499,8 +401,7 @@ msgid ""
|
||||
" Sharing the recording via link is not yet available. Only organizers can "
|
||||
"download it. "
|
||||
msgstr ""
|
||||
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls "
|
||||
"les organisateurs peuvent le télécharger."
|
||||
"Le partage de l'enregistrement via lien n'est pas encore disponible. Seuls les organisateurs peuvent le télécharger."
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:206
|
||||
#: core/templates/mail/text/screen_recording.txt:11
|
||||
@@ -537,18 +438,18 @@ msgstr ""
|
||||
" Si vous avez des questions ou besoin d'assistance, veuillez contacter notre "
|
||||
"équipe d'assistance à %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:169
|
||||
#: meet/settings.py:163
|
||||
msgid "English"
|
||||
msgstr "Anglais"
|
||||
|
||||
#: meet/settings.py:170
|
||||
#: meet/settings.py:164
|
||||
msgid "French"
|
||||
msgstr "Français"
|
||||
|
||||
#: meet/settings.py:171
|
||||
#: meet/settings.py:165
|
||||
msgid "Dutch"
|
||||
msgstr "Néerlandais"
|
||||
|
||||
#: meet/settings.py:172
|
||||
#: meet/settings.py:166
|
||||
msgid "German"
|
||||
msgstr "Allemand"
|
||||
|
||||
Binary file not shown.
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-12-29 15:15+0000\n"
|
||||
"POT-Creation-Date: 2025-07-11 11:33+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -17,152 +17,120 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
#: core/admin.py:29
|
||||
#: core/admin.py:26
|
||||
msgid "Personal info"
|
||||
msgstr "Persoonlijke informatie"
|
||||
|
||||
#: core/admin.py:42
|
||||
#: core/admin.py:39
|
||||
msgid "Permissions"
|
||||
msgstr "Rechten"
|
||||
|
||||
#: core/admin.py:54
|
||||
#: core/admin.py:51
|
||||
msgid "Important dates"
|
||||
msgstr "Belangrijke datums"
|
||||
|
||||
#: core/admin.py:128 core/admin.py:228
|
||||
#: core/admin.py:147
|
||||
msgid "No owner"
|
||||
msgstr "Geen eigenaar"
|
||||
|
||||
#: core/admin.py:131 core/admin.py:231
|
||||
#: core/admin.py:150
|
||||
msgid "Multiple owners"
|
||||
msgstr "Meerdere eigenaren"
|
||||
|
||||
#: core/admin.py:143
|
||||
msgid "Resend notification to external service"
|
||||
msgstr "Melding opnieuw verzenden naar externe dienst"
|
||||
|
||||
#: core/admin.py:166
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s"
|
||||
msgstr "Melding voor opname %(id)s mislukt"
|
||||
|
||||
#: core/admin.py:174
|
||||
#, python-format
|
||||
msgid "Failed to notify for recording %(id)s: %(error)s"
|
||||
msgstr "Melding voor opname %(id)s mislukt: %(error)s"
|
||||
|
||||
#: core/admin.py:182
|
||||
#, python-format
|
||||
msgid "Successfully sent notifications for %(count)s recording(s)."
|
||||
msgstr "Meldingen succesvol verzonden voor %(count)s opname(n)."
|
||||
|
||||
#: core/admin.py:190
|
||||
#, python-format
|
||||
msgid "Skipped %(count)s expired recording(s)."
|
||||
msgstr "%(count)s verlopen opname(n) overgeslagen."
|
||||
|
||||
#: core/admin.py:294
|
||||
msgid "No scopes"
|
||||
msgstr "Geen scopes"
|
||||
|
||||
#: core/admin.py:296
|
||||
msgid "Scopes"
|
||||
msgstr "Scopes"
|
||||
|
||||
#: core/api/serializers.py:68
|
||||
#: core/api/serializers.py:67
|
||||
msgid "You must be administrator or owner of a room to add accesses to it."
|
||||
msgstr ""
|
||||
"Je moet beheerder of eigenaar van een ruimte zijn om toegang toe te voegen."
|
||||
|
||||
#: core/models.py:34
|
||||
#: core/models.py:31
|
||||
msgid "Member"
|
||||
msgstr "Lid"
|
||||
|
||||
#: core/models.py:35
|
||||
#: core/models.py:32
|
||||
msgid "Administrator"
|
||||
msgstr "Beheerder"
|
||||
|
||||
#: core/models.py:36
|
||||
#: core/models.py:33
|
||||
msgid "Owner"
|
||||
msgstr "Eigenaar"
|
||||
|
||||
#: core/models.py:52
|
||||
#: core/models.py:49
|
||||
msgid "Initiated"
|
||||
msgstr "Gestart"
|
||||
|
||||
#: core/models.py:53
|
||||
#: core/models.py:50
|
||||
msgid "Active"
|
||||
msgstr "Actief"
|
||||
|
||||
#: core/models.py:54
|
||||
#: core/models.py:51
|
||||
msgid "Stopped"
|
||||
msgstr "Gestopt"
|
||||
|
||||
#: core/models.py:55
|
||||
#: core/models.py:52
|
||||
msgid "Saved"
|
||||
msgstr "Opgeslagen"
|
||||
|
||||
#: core/models.py:56
|
||||
#: core/models.py:53
|
||||
msgid "Aborted"
|
||||
msgstr "Afgebroken"
|
||||
|
||||
#: core/models.py:57
|
||||
#: core/models.py:54
|
||||
msgid "Failed to Start"
|
||||
msgstr "Starten mislukt"
|
||||
|
||||
#: core/models.py:58
|
||||
#: core/models.py:55
|
||||
msgid "Failed to Stop"
|
||||
msgstr "Stoppen mislukt"
|
||||
|
||||
#: core/models.py:59
|
||||
#: core/models.py:56
|
||||
msgid "Notification succeeded"
|
||||
msgstr "Notificatie geslaagd"
|
||||
|
||||
#: core/models.py:86
|
||||
#: core/models.py:83
|
||||
msgid "SCREEN_RECORDING"
|
||||
msgstr "SCHERM_OPNAME"
|
||||
|
||||
#: core/models.py:87
|
||||
#: core/models.py:84
|
||||
msgid "TRANSCRIPT"
|
||||
msgstr "TRANSCRIPT"
|
||||
|
||||
#: core/models.py:93
|
||||
#: core/models.py:90
|
||||
msgid "Public Access"
|
||||
msgstr "Openbare toegang"
|
||||
|
||||
#: core/models.py:94
|
||||
#: core/models.py:91
|
||||
msgid "Trusted Access"
|
||||
msgstr "Vertrouwde toegang"
|
||||
|
||||
#: core/models.py:95
|
||||
#: core/models.py:92
|
||||
msgid "Restricted Access"
|
||||
msgstr "Beperkte toegang"
|
||||
|
||||
#: core/models.py:107
|
||||
#: core/models.py:104
|
||||
msgid "id"
|
||||
msgstr "id"
|
||||
|
||||
#: core/models.py:108
|
||||
#: core/models.py:105
|
||||
msgid "primary key for the record as UUID"
|
||||
msgstr "primaire sleutel voor het record als UUID"
|
||||
|
||||
#: core/models.py:114
|
||||
#: core/models.py:111
|
||||
msgid "created on"
|
||||
msgstr "aangemaakt op"
|
||||
|
||||
#: core/models.py:115
|
||||
#: core/models.py:112
|
||||
msgid "date and time at which a record was created"
|
||||
msgstr "datum en tijd waarop een record werd aangemaakt"
|
||||
|
||||
#: core/models.py:120
|
||||
#: core/models.py:117
|
||||
msgid "updated on"
|
||||
msgstr "bijgewerkt op"
|
||||
|
||||
#: core/models.py:121
|
||||
#: core/models.py:118
|
||||
msgid "date and time at which a record was last updated"
|
||||
msgstr "datum en tijd waarop een record voor het laatst werd bijgewerkt"
|
||||
|
||||
#: core/models.py:141
|
||||
#: core/models.py:138
|
||||
msgid ""
|
||||
"Enter a valid sub. This value may contain only letters, numbers, and @/./+/-/"
|
||||
"_ characters."
|
||||
@@ -170,67 +138,66 @@ msgstr ""
|
||||
"Voer een geldige sub in. Deze waarde mag alleen letters, cijfers en @/./+/-/"
|
||||
"_ tekens bevatten."
|
||||
|
||||
#: core/models.py:147
|
||||
#: core/models.py:144
|
||||
msgid "sub"
|
||||
msgstr "sub"
|
||||
|
||||
#: core/models.py:149
|
||||
#: core/models.py:146
|
||||
msgid ""
|
||||
"Optional for pending users; required upon account activation. 255 characters "
|
||||
"or fewer. Letters, numbers, and @/./+/-/_ characters only."
|
||||
"Required. 255 characters or fewer. Letters, numbers, and @/./+/-/_ "
|
||||
"characters only."
|
||||
msgstr ""
|
||||
"Optioneel voor gebruikers in afwachting; vereist bij accountactivering. "
|
||||
"Maximum 255 tekens. Alleen letters, cijfers en @/./+/-/_ toegestaan."
|
||||
"Vereist. 255 tekens of minder. Alleen letters, cijfers en @/./+/-/_ tekens."
|
||||
|
||||
#: core/models.py:158
|
||||
#: core/models.py:154
|
||||
msgid "identity email address"
|
||||
msgstr "identiteit e-mailadres"
|
||||
|
||||
#: core/models.py:163
|
||||
#: core/models.py:159
|
||||
msgid "admin email address"
|
||||
msgstr "beheerder e-mailadres"
|
||||
|
||||
#: core/models.py:165
|
||||
#: core/models.py:161
|
||||
msgid "full name"
|
||||
msgstr "volledige naam"
|
||||
|
||||
#: core/models.py:167
|
||||
#: core/models.py:163
|
||||
msgid "short name"
|
||||
msgstr "korte naam"
|
||||
|
||||
#: core/models.py:173
|
||||
#: core/models.py:169
|
||||
msgid "language"
|
||||
msgstr "taal"
|
||||
|
||||
#: core/models.py:174
|
||||
#: core/models.py:170
|
||||
msgid "The language in which the user wants to see the interface."
|
||||
msgstr "De taal waarin de gebruiker de interface wil zien."
|
||||
|
||||
#: core/models.py:180
|
||||
#: core/models.py:176
|
||||
msgid "The timezone in which the user wants to see times."
|
||||
msgstr "De tijdzone waarin de gebruiker tijden wil zien."
|
||||
|
||||
#: core/models.py:183
|
||||
#: core/models.py:179
|
||||
msgid "device"
|
||||
msgstr "apparaat"
|
||||
|
||||
#: core/models.py:185
|
||||
#: core/models.py:181
|
||||
msgid "Whether the user is a device or a real user."
|
||||
msgstr "Of de gebruiker een apparaat is of een echte gebruiker."
|
||||
|
||||
#: core/models.py:188
|
||||
#: core/models.py:184
|
||||
msgid "staff status"
|
||||
msgstr "personeelsstatus"
|
||||
|
||||
#: core/models.py:190
|
||||
#: core/models.py:186
|
||||
msgid "Whether the user can log into this admin site."
|
||||
msgstr "Of de gebruiker kan inloggen op deze beheersite."
|
||||
|
||||
#: core/models.py:193
|
||||
#: core/models.py:189
|
||||
msgid "active"
|
||||
msgstr "actief"
|
||||
|
||||
#: core/models.py:196
|
||||
#: core/models.py:192
|
||||
msgid ""
|
||||
"Whether this user should be treated as active. Unselect this instead of "
|
||||
"deleting accounts."
|
||||
@@ -238,64 +205,64 @@ msgstr ""
|
||||
"Of deze gebruiker als actief moet worden behandeld. Deselecteer dit in "
|
||||
"plaats van accounts te verwijderen."
|
||||
|
||||
#: core/models.py:209
|
||||
#: core/models.py:205
|
||||
msgid "user"
|
||||
msgstr "gebruiker"
|
||||
|
||||
#: core/models.py:210
|
||||
#: core/models.py:206
|
||||
msgid "users"
|
||||
msgstr "gebruikers"
|
||||
|
||||
#: core/models.py:269
|
||||
#: core/models.py:265
|
||||
msgid "Resource"
|
||||
msgstr "Bron"
|
||||
|
||||
#: core/models.py:270
|
||||
#: core/models.py:266
|
||||
msgid "Resources"
|
||||
msgstr "Bronnen"
|
||||
|
||||
#: core/models.py:324
|
||||
#: core/models.py:320
|
||||
msgid "Resource access"
|
||||
msgstr "Brontoegang"
|
||||
|
||||
#: core/models.py:325
|
||||
#: core/models.py:321
|
||||
msgid "Resource accesses"
|
||||
msgstr "Brontoegangsrechten"
|
||||
|
||||
#: core/models.py:331
|
||||
#: core/models.py:327
|
||||
msgid "Resource access with this User and Resource already exists."
|
||||
msgstr "Brontoegang met deze gebruiker en bron bestaat al."
|
||||
|
||||
#: core/models.py:387
|
||||
#: core/models.py:383
|
||||
msgid "Visio room configuration"
|
||||
msgstr "Visio-ruimteconfiguratie"
|
||||
|
||||
#: core/models.py:388
|
||||
#: core/models.py:384
|
||||
msgid "Values for Visio parameters to configure the room."
|
||||
msgstr "Waarden voor Visio-parameters om de ruimte te configureren."
|
||||
|
||||
#: core/models.py:395
|
||||
#: core/models.py:391
|
||||
msgid "Room PIN code"
|
||||
msgstr "Pincode van de kamer"
|
||||
|
||||
#: core/models.py:396
|
||||
#: core/models.py:392
|
||||
msgid "Unique n-digit code that identifies this room in telephony mode."
|
||||
msgstr ""
|
||||
"Unieke n-cijferige code die deze kamer identificeert in telefonie-modus."
|
||||
|
||||
#: core/models.py:402 core/models.py:556
|
||||
#: core/models.py:398 core/models.py:552
|
||||
msgid "Room"
|
||||
msgstr "Ruimte"
|
||||
|
||||
#: core/models.py:403
|
||||
#: core/models.py:399
|
||||
msgid "Rooms"
|
||||
msgstr "Ruimtes"
|
||||
|
||||
#: core/models.py:567
|
||||
#: core/models.py:563
|
||||
msgid "Worker ID"
|
||||
msgstr "Worker ID"
|
||||
|
||||
#: core/models.py:569
|
||||
#: core/models.py:565
|
||||
msgid ""
|
||||
"Enter an identifier for the worker recording.This ID is retained even when "
|
||||
"the worker stops, allowing for easy tracking."
|
||||
@@ -303,107 +270,42 @@ msgstr ""
|
||||
"Voer een identificatie in voor de worker-opname. Deze ID blijft behouden, "
|
||||
"zelfs wanneer de worker stopt, waardoor eenvoudige tracking mogelijk is."
|
||||
|
||||
#: core/models.py:577
|
||||
#: core/models.py:573
|
||||
msgid "Recording mode"
|
||||
msgstr "Opnamemodus"
|
||||
|
||||
#: core/models.py:578
|
||||
#: core/models.py:574
|
||||
msgid "Defines the mode of recording being called."
|
||||
msgstr "Definieert de modus van opname die wordt aangeroepen."
|
||||
|
||||
#: core/models.py:583 core/models.py:584
|
||||
msgid "Recording options"
|
||||
msgstr "Opnameopties"
|
||||
|
||||
#: core/models.py:590
|
||||
#: core/models.py:580
|
||||
msgid "Recording"
|
||||
msgstr "Opname"
|
||||
|
||||
#: core/models.py:591
|
||||
#: core/models.py:581
|
||||
msgid "Recordings"
|
||||
msgstr "Opnames"
|
||||
|
||||
#: core/models.py:699
|
||||
#: core/models.py:689
|
||||
msgid "Recording/user relation"
|
||||
msgstr "Opname/gebruiker-relatie"
|
||||
|
||||
#: core/models.py:700
|
||||
#: core/models.py:690
|
||||
msgid "Recording/user relations"
|
||||
msgstr "Opname/gebruiker-relaties"
|
||||
|
||||
#: core/models.py:706
|
||||
#: core/models.py:696
|
||||
msgid "This user is already in this recording."
|
||||
msgstr "Deze gebruiker is al in deze opname."
|
||||
|
||||
#: core/models.py:712
|
||||
#: core/models.py:702
|
||||
msgid "This team is already in this recording."
|
||||
msgstr "Dit team is al in deze opname."
|
||||
|
||||
#: core/models.py:718
|
||||
#: core/models.py:708
|
||||
msgid "Either user or team must be set, not both."
|
||||
msgstr "Ofwel gebruiker of team moet worden ingesteld, niet beide."
|
||||
|
||||
#: core/models.py:735
|
||||
msgid "Create rooms"
|
||||
msgstr "Ruimtes aanmaken"
|
||||
|
||||
#: core/models.py:736
|
||||
msgid "List rooms"
|
||||
msgstr "Ruimtes weergeven"
|
||||
|
||||
#: core/models.py:737
|
||||
msgid "Retrieve room details"
|
||||
msgstr "Details van een ruimte ophalen"
|
||||
|
||||
#: core/models.py:738
|
||||
msgid "Update rooms"
|
||||
msgstr "Ruimtes bijwerken"
|
||||
|
||||
#: core/models.py:739
|
||||
msgid "Delete rooms"
|
||||
msgstr "Ruimtes verwijderen"
|
||||
|
||||
#: core/models.py:752
|
||||
msgid "Application name"
|
||||
msgstr "Naam van de applicatie"
|
||||
|
||||
#: core/models.py:753
|
||||
msgid "Descriptive name for this application."
|
||||
msgstr "Beschrijvende naam voor deze applicatie."
|
||||
|
||||
#: core/models.py:763
|
||||
msgid "Hashed on Save. Copy it now if this is a new secret."
|
||||
msgstr ""
|
||||
"Wordt gehasht bij het opslaan. Kopieer het nu als dit een nieuw geheim is."
|
||||
|
||||
#: core/models.py:774
|
||||
msgid "Application"
|
||||
msgstr "Applicatie"
|
||||
|
||||
#: core/models.py:775
|
||||
msgid "Applications"
|
||||
msgstr "Applicaties"
|
||||
|
||||
#: core/models.py:798
|
||||
msgid "Enter a valid domain"
|
||||
msgstr "Voer een geldig domein in"
|
||||
|
||||
#: core/models.py:801
|
||||
msgid "Domain"
|
||||
msgstr "Domein"
|
||||
|
||||
#: core/models.py:802
|
||||
msgid "Email domain this application can act on behalf of."
|
||||
msgstr "E-maildomein namens welke deze applicatie kan handelen."
|
||||
|
||||
#: core/models.py:814
|
||||
msgid "Application domain"
|
||||
msgstr "Applicatiedomein"
|
||||
|
||||
#: core/models.py:815
|
||||
msgid "Application domains"
|
||||
msgstr "Applicatiedomeinen"
|
||||
|
||||
#: core/recording/event/notification.py:94
|
||||
msgid "Your recording is ready"
|
||||
msgstr "Je opname is klaar"
|
||||
@@ -494,8 +396,7 @@ msgid ""
|
||||
" Sharing the recording via link is not yet available. Only organizers can "
|
||||
"download it. "
|
||||
msgstr ""
|
||||
"Het delen van de opname via een link is nog niet beschikbaar. Alleen "
|
||||
"organisatoren kunnen deze downloaden."
|
||||
"Het delen van de opname via een link is nog niet beschikbaar. Alleen organisatoren kunnen deze downloaden."
|
||||
|
||||
#: core/templates/mail/html/screen_recording.html:206
|
||||
#: core/templates/mail/text/screen_recording.txt:11
|
||||
@@ -532,18 +433,18 @@ msgstr ""
|
||||
" Als je vragen hebt of hulp nodig hebt, neem dan contact op met ons support "
|
||||
"team via %(support_email)s. "
|
||||
|
||||
#: meet/settings.py:169
|
||||
#: meet/settings.py:163
|
||||
msgid "English"
|
||||
msgstr "Engels"
|
||||
|
||||
#: meet/settings.py:170
|
||||
#: meet/settings.py:164
|
||||
msgid "French"
|
||||
msgstr "Frans"
|
||||
|
||||
#: meet/settings.py:171
|
||||
#: meet/settings.py:165
|
||||
msgid "Dutch"
|
||||
msgstr "Nederlands"
|
||||
|
||||
#: meet/settings.py:172
|
||||
#: meet/settings.py:166
|
||||
msgid "German"
|
||||
msgstr "Duits"
|
||||
|
||||
@@ -10,15 +10,12 @@ For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||
"""
|
||||
|
||||
# pylint: disable=too-many-lines
|
||||
|
||||
import json
|
||||
from os import path
|
||||
from socket import gethostbyname, gethostname
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
import dj_database_url
|
||||
import sentry_sdk
|
||||
from configurations import Configuration, values
|
||||
from lasuite.configuration.values import SecretFileValue
|
||||
@@ -93,11 +90,7 @@ class Base(Configuration):
|
||||
|
||||
# Database
|
||||
DATABASES = {
|
||||
"default": dj_database_url.config()
|
||||
if values.DatabaseURLValue(
|
||||
None, environ_name="DATABASE_URL", environ_prefix=None
|
||||
)
|
||||
else {
|
||||
"default": {
|
||||
"ENGINE": values.Value(
|
||||
"django.db.backends.postgresql_psycopg2",
|
||||
environ_name="DB_ENGINE",
|
||||
@@ -297,9 +290,6 @@ class Base(Configuration):
|
||||
),
|
||||
},
|
||||
}
|
||||
MONITORED_THROTTLE_FAILURE_CALLBACK = (
|
||||
"core.api.throttling.sentry_monitoring_throttle_failure"
|
||||
)
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Meet API",
|
||||
@@ -336,29 +326,21 @@ class Base(Configuration):
|
||||
"is_silent_login_enabled": values.BooleanValue(
|
||||
True, environ_name="FRONTEND_IS_SILENT_LOGIN_ENABLED", environ_prefix=None
|
||||
),
|
||||
"idle_disconnect_warning_delay": values.PositiveIntegerValue(
|
||||
None,
|
||||
environ_name="FRONTEND_IDLE_DISCONNECT_WARNING_DELAY",
|
||||
environ_prefix=None,
|
||||
),
|
||||
"feedback": values.DictValue(
|
||||
{}, environ_name="FRONTEND_FEEDBACK", environ_prefix=None
|
||||
),
|
||||
"external_home_url": values.Value(
|
||||
None, environ_name="FRONTEND_EXTERNAL_HOME_URL", environ_prefix=None
|
||||
),
|
||||
"use_french_gov_footer": values.BooleanValue(
|
||||
False, environ_name="FRONTEND_USE_FRENCH_GOV_FOOTER", environ_prefix=None
|
||||
),
|
||||
"use_proconnect_button": values.BooleanValue(
|
||||
False, environ_name="FRONTEND_USE_PROCONNECT_BUTTON", environ_prefix=None
|
||||
),
|
||||
"transcript": values.DictValue(
|
||||
{}, environ_name="FRONTEND_TRANSCRIPT", environ_prefix=None
|
||||
),
|
||||
"manifest_link": values.Value(
|
||||
None, environ_name="FRONTEND_MANIFEST_LINK", environ_prefix=None
|
||||
),
|
||||
"transcription_destination": values.Value(
|
||||
None, environ_name="FRONTEND_TRANSCRIPTION_DESTINATION", environ_prefix=None
|
||||
),
|
||||
}
|
||||
|
||||
# Mail
|
||||
@@ -416,15 +398,7 @@ class Base(Configuration):
|
||||
OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION = values.BooleanValue(
|
||||
default=False,
|
||||
environ_name="OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_USER_SUB_FIELD_IMMUTABLE = values.BooleanValue(
|
||||
default=True, environ_name="OIDC_USER_SUB_FIELD_IMMUTABLE", environ_prefix=None
|
||||
)
|
||||
OIDC_TIMEOUT = values.IntegerValue(
|
||||
5, environ_name="OIDC_TIMEOUT", environ_prefix=None
|
||||
)
|
||||
OIDC_PROXY = values.Value(None, environ_name="OIDC_PROXY", environ_prefix=None)
|
||||
OIDC_RP_SIGN_ALGO = values.Value(
|
||||
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
|
||||
)
|
||||
@@ -448,16 +422,12 @@ class Base(Configuration):
|
||||
OIDC_OP_USER_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_INTROSPECTION_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_INTROSPECTION_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_USER_ENDPOINT_FORMAT = values.Value(
|
||||
"AUTO", environ_name="OIDC_OP_USER_ENDPOINT_FORMAT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_LOGOUT_ENDPOINT = values.Value(
|
||||
None, environ_name="OIDC_OP_LOGOUT_ENDPOINT", environ_prefix=None
|
||||
)
|
||||
OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None)
|
||||
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
|
||||
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
|
||||
)
|
||||
@@ -518,45 +488,6 @@ class Base(Configuration):
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# OIDC Resource Server Backend
|
||||
OIDC_RS_BACKEND_CLASS = "core.external_api.authentication.ResourceServerBackend"
|
||||
OIDC_RS_CLIENT_ID = values.Value(
|
||||
"meet", environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_CLIENT_SECRET = SecretFileValue(
|
||||
None,
|
||||
environ_name="OIDC_RS_CLIENT_SECRET",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_AUDIENCE_CLAIM = values.Value(
|
||||
default="client_id", environ_name="OIDC_RS_AUDIENCE_CLAIM", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
|
||||
default="A256GCM",
|
||||
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_ALGO = values.Value(
|
||||
default="RSA-OAEP", environ_name="OIDC_RS_ENCRYPTION_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SIGNING_ALGO = values.Value(
|
||||
default="ES256", environ_name="OIDC_RS_SIGNING_ALGO", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SCOPES = values.ListValue(
|
||||
default=["lasuite_meet"],
|
||||
environ_name="OIDC_RS_SCOPES",
|
||||
environ_prefix=None,
|
||||
)
|
||||
OIDC_RS_PRIVATE_KEY_STR = SecretFileValue(
|
||||
environ_name="OIDC_RS_PRIVATE_KEY_STR", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
|
||||
default="RSA", environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE", environ_prefix=None
|
||||
)
|
||||
OIDC_RS_SCOPES_PREFIX = values.Value(
|
||||
default=None, environ_name="OIDC_RS_SCOPES_PREFIX", environ_prefix=None
|
||||
)
|
||||
|
||||
# Video conference configuration
|
||||
LIVEKIT_CONFIGURATION = {
|
||||
"api_key": SecretFileValue(environ_name="LIVEKIT_API_KEY", environ_prefix=None),
|
||||
@@ -645,9 +576,6 @@ class Base(Configuration):
|
||||
SCREEN_RECORDING_BASE_URL = values.Value(
|
||||
None, environ_name="SCREEN_RECORDING_BASE_URL", environ_prefix=None
|
||||
)
|
||||
RECORDING_DOWNLOAD_BASE_URL = values.Value(
|
||||
None, environ_name="RECORDING_DOWNLOAD_BASE_URL", environ_prefix=None
|
||||
)
|
||||
|
||||
# Marketing and communication settings
|
||||
SIGNUP_NEW_USER_TO_MARKETING_EMAIL = values.BooleanValue(
|
||||
@@ -788,14 +716,6 @@ class Base(Configuration):
|
||||
environ_name="APPLICATION_BASE_URL",
|
||||
environ_prefix=None,
|
||||
)
|
||||
# Allows third-party platforms to create users with email-only identification.
|
||||
# Required for external integrations, but fragile due to deferred user reconciliation
|
||||
# on sub. Enable it with care /!\
|
||||
APPLICATION_ALLOW_USER_CREATION = values.BooleanValue(
|
||||
False,
|
||||
environ_name="APPLICATION_ALLOW_USER_CREATION",
|
||||
environ_prefix=None,
|
||||
)
|
||||
|
||||
# pylint: disable=invalid-name
|
||||
@property
|
||||
|
||||
@@ -15,7 +15,6 @@ from drf_spectacular.views import (
|
||||
urlpatterns = [
|
||||
path("admin/", admin.site.urls),
|
||||
path("", include("core.urls")),
|
||||
path("", include("lasuite.oidc_resource_server.urls")),
|
||||
]
|
||||
|
||||
if settings.DEBUG:
|
||||
|
||||
+31
-32
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "meet"
|
||||
version = "1.6.0"
|
||||
version = "0.1.40"
|
||||
authors = [{ "name" = "DINUM", "email" = "dev@mail.numerique.gouv.fr" }]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
@@ -25,39 +25,38 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.40.69",
|
||||
"Brotli==1.2.0",
|
||||
"brevo-python==1.2.0",
|
||||
"boto3==1.38.42",
|
||||
"Brotli==1.1.0",
|
||||
"brevo-python==1.1.2",
|
||||
"celery[redis]==5.5.3",
|
||||
"dj-database-url==3.1.0",
|
||||
"django-configurations==2.5.1",
|
||||
"django-cors-headers==4.9.0",
|
||||
"django-countries==8.0.0",
|
||||
"django-lasuite[all]==0.0.19",
|
||||
"django-cors-headers==4.7.0",
|
||||
"django-countries==7.6.1",
|
||||
"django-lasuite[all]==0.0.10",
|
||||
"django-parler==2.3",
|
||||
"redis==5.2.1",
|
||||
"django-redis==6.0.0",
|
||||
"django-storages[s3]==1.14.6",
|
||||
"django-timezone-field>=5.1",
|
||||
"django==5.2.11",
|
||||
"djangorestframework==3.16.1",
|
||||
"drf_spectacular==0.29.0",
|
||||
"django==5.2.7",
|
||||
"djangorestframework==3.16.0",
|
||||
"drf_spectacular==0.28.0",
|
||||
"dockerflow==2024.4.2",
|
||||
"easy_thumbnails==2.10.1",
|
||||
"easy_thumbnails==2.10",
|
||||
"factory_boy==3.3.3",
|
||||
"gunicorn==23.0.0",
|
||||
"jsonschema==4.25.1",
|
||||
"markdown==3.10",
|
||||
"nested-multipart-parser==1.6.0",
|
||||
"psycopg[binary]==3.2.12",
|
||||
"jsonschema==4.24.0",
|
||||
"markdown==3.8.2",
|
||||
"nested-multipart-parser==1.5.0",
|
||||
"psycopg[binary]==3.2.9",
|
||||
"PyJWT==2.10.1",
|
||||
"python-frontmatter==1.1.0",
|
||||
"requests==2.32.5",
|
||||
"sentry-sdk==2.43.0",
|
||||
"whitenoise==6.11.0",
|
||||
"requests==2.32.4",
|
||||
"sentry-sdk==2.30.0",
|
||||
"whitenoise==6.9.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
"livekit-api==1.0.7",
|
||||
"aiohttp==3.13.3",
|
||||
"livekit-api==1.0.3",
|
||||
"aiohttp==3.12.13",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
@@ -69,21 +68,21 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"django-extensions==4.1",
|
||||
"drf-spectacular-sidecar==2025.10.1",
|
||||
"freezegun==1.5.5",
|
||||
"drf-spectacular-sidecar==2025.6.1",
|
||||
"freezegun==1.5.2",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==9.7.0",
|
||||
"pyfakefs==5.10.2",
|
||||
"ipython==9.3.0",
|
||||
"pyfakefs==5.9.1",
|
||||
"pylint-django==2.6.1",
|
||||
"pylint<4.0.0",
|
||||
"pytest-cov==7.0.0",
|
||||
"pylint==3.3.7",
|
||||
"pytest-cov==6.2.1",
|
||||
"pytest-django==4.11.1",
|
||||
"pytest==9.0.0",
|
||||
"pytest==8.4.1",
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.8.0",
|
||||
"responses==0.25.8",
|
||||
"ruff==0.14.4",
|
||||
"types-requests==2.32.4.20250913",
|
||||
"pytest-xdist==3.7.0",
|
||||
"responses==0.25.7",
|
||||
"ruff==0.12.0",
|
||||
"types-requests==2.32.4.20250611",
|
||||
]
|
||||
|
||||
[tool.setuptools]
|
||||
|
||||
@@ -38,12 +38,7 @@ RUN npm run build
|
||||
FROM nginxinc/nginx-unprivileged:alpine3.21 AS frontend-production
|
||||
|
||||
USER root
|
||||
RUN apk update && apk upgrade libssl3 \
|
||||
libcrypto3 \
|
||||
libxml2>=2.12.7-r2 \
|
||||
libxslt>=1.1.39-r2 \
|
||||
libexpat>=2.7.2-r0 \
|
||||
libpng>=1.6.53-r0
|
||||
RUN apk update && apk upgrade libssl3 libcrypto3 libxml2>=2.12.7-r2 libxslt>=1.1.39-r2 libexpat>=2.7.2-r0
|
||||
|
||||
USER nginx
|
||||
|
||||
|
||||
@@ -5,12 +5,6 @@ server {
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
location = /.wellknown/windows-app-web-link {
|
||||
default_type application/json;
|
||||
alias /usr/share/nginx/html/.wellknown/windows-app-web-link;
|
||||
add_header Content-Disposition "attachment; filename=windows-app-web-link";
|
||||
}
|
||||
|
||||
# Serve static files with caching
|
||||
location ~* ^/assets/.*\.(css|js|json|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 30d;
|
||||
|
||||
@@ -6,22 +6,6 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<!-- Font URLs are resolved and replaced by Vite during the build process. Font loading failures will not break the application. -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin="anonymous"
|
||||
href="/node_modules/@fontsource/material-icons-outlined/files/material-icons-outlined-latin-400-normal.woff2"
|
||||
type="font/woff2"
|
||||
/>
|
||||
<!-- Font URLs are resolved and replaced by Vite during the build process. Font loading failures will not break the application. -->
|
||||
<link
|
||||
rel="preload"
|
||||
as="font"
|
||||
crossorigin="anonymous"
|
||||
href="/node_modules/@fontsource-variable/material-symbols-outlined/files/material-symbols-outlined-latin-wght-normal.woff2"
|
||||
type="font/woff2"
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>%VITE_APP_TITLE%</title>
|
||||
</head>
|
||||
|
||||
Generated
+172
-545
File diff suppressed because it is too large
Load Diff
+11
-13
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "meet",
|
||||
"private": true,
|
||||
"version": "1.6.0",
|
||||
"version": "0.1.40",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "panda codegen && vite",
|
||||
@@ -13,35 +13,33 @@
|
||||
"check": "prettier --check ./src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource-variable/material-symbols-outlined": "5.2.34",
|
||||
"@fontsource/material-icons-outlined": "5.2.6",
|
||||
"@livekit/components-react": "2.9.13",
|
||||
"@livekit/components-styles": "1.1.6",
|
||||
"@livekit/track-processors": "0.6.1",
|
||||
"@pandacss/preset-panda": "0.54.0",
|
||||
"@react-aria/toast": "3.0.10",
|
||||
"@react-types/overlays": "3.9.3",
|
||||
"@react-aria/toast": "3.0.5",
|
||||
"@react-types/overlays": "3.9.0",
|
||||
"@remixicon/react": "4.6.0",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
"@timephy/rnnoise-wasm": "1.0.0",
|
||||
"crisp-sdk-web": "1.0.27",
|
||||
"crisp-sdk-web": "1.0.25",
|
||||
"derive-valtio": "0.2.0",
|
||||
"hoofd": "1.7.3",
|
||||
"humanize-duration": "3.33.2",
|
||||
"i18next": "25.8.4",
|
||||
"humanize-duration": "3.33.0",
|
||||
"i18next": "25.3.1",
|
||||
"i18next-browser-languagedetector": "8.2.0",
|
||||
"i18next-parser": "9.3.0",
|
||||
"i18next-resources-to-backend": "1.2.1",
|
||||
"libphonenumber-js": "1.12.10",
|
||||
"livekit-client": "2.15.7",
|
||||
"posthog-js": "1.342.1",
|
||||
"posthog-js": "1.256.2",
|
||||
"react": "18.3.1",
|
||||
"react-aria-components": "1.10.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "15.1.1",
|
||||
"use-sound": "5.0.0",
|
||||
"valtio": "2.3.0",
|
||||
"wouter": "3.9.0"
|
||||
"valtio": "2.1.5",
|
||||
"wouter": "3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pandacss/dev": "0.54.0",
|
||||
@@ -60,9 +58,9 @@
|
||||
"eslint-plugin-react-hooks": "5.2.0",
|
||||
"eslint-plugin-react-refresh": "0.4.20",
|
||||
"postcss": "8.5.6",
|
||||
"prettier": "3.8.1",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.8.3",
|
||||
"vite": "7.0.8",
|
||||
"vite": "7.0.2",
|
||||
"vite-tsconfig-paths": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
[
|
||||
{
|
||||
"packageFamilyName" : "Visio_g3z6ba6vek6vg",
|
||||
"paths" : [ "*" ]
|
||||
}
|
||||
]
|
||||
@@ -17,13 +17,14 @@ export interface ApiConfig {
|
||||
feedback: {
|
||||
url: string
|
||||
}
|
||||
external_home_url?: string
|
||||
transcript: {
|
||||
form_beta_users: string
|
||||
}
|
||||
silence_livekit_debug_logs?: boolean
|
||||
is_silent_login_enabled?: boolean
|
||||
custom_css_url?: string
|
||||
use_french_gov_footer?: boolean
|
||||
use_proconnect_button?: boolean
|
||||
idle_disconnect_warning_delay?: number
|
||||
recording?: {
|
||||
is_enabled?: boolean
|
||||
available_modes?: RecordingMode[]
|
||||
@@ -45,7 +46,6 @@ export interface ApiConfig {
|
||||
enable_firefox_proxy_workaround: boolean
|
||||
default_sources: string[]
|
||||
}
|
||||
transcription_destination?: string
|
||||
}
|
||||
|
||||
const fetchConfig = (): Promise<ApiConfig> => {
|
||||
|
||||
@@ -68,7 +68,6 @@ export const Avatar = ({
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={css({
|
||||
marginTop: '-0.3rem',
|
||||
})}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -6,7 +6,6 @@ export const BlurOnStrong = () => {
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
|
||||
@@ -5,6 +5,6 @@ export const authUrl = ({
|
||||
returnTo = window.location.href,
|
||||
} = {}) => {
|
||||
return apiUrl(
|
||||
`/authenticate/?silent=${encodeURIComponent(silent)}&returnTo=${encodeURIComponent(returnTo)}`
|
||||
`/authenticate?silent=${encodeURIComponent(silent)}&returnTo=${encodeURIComponent(returnTo)}`
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { styled } from '@/styled-system/jsx'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { Button } from '@/primitives'
|
||||
import { Button, LinkButton } from '@/primitives'
|
||||
import { RiArrowLeftSLine, RiArrowRightSLine } from '@remixicon/react'
|
||||
import { useState } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
|
||||
const Heading = styled('h2', {
|
||||
base: {
|
||||
@@ -164,7 +165,14 @@ export const IntroSlider = () => {
|
||||
const [slideIndex, setSlideIndex] = useState(0)
|
||||
const { t } = useTranslation('home', { keyPrefix: 'introSlider' })
|
||||
|
||||
const NUMBER_SLIDES = SLIDES.length
|
||||
const { data } = useConfig()
|
||||
|
||||
const filteredSlides = useMemo(
|
||||
() => (data?.transcript?.form_beta_users ? SLIDES : SLIDES.slice(0, 2)),
|
||||
[data]
|
||||
)
|
||||
|
||||
const NUMBER_SLIDES = filteredSlides.length
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -190,12 +198,24 @@ export const IntroSlider = () => {
|
||||
</ButtonVerticalCenter>
|
||||
</ButtonContainer>
|
||||
<SlideContainer>
|
||||
{SLIDES.map((slide, index) => (
|
||||
{filteredSlides.map((slide, index) => (
|
||||
<Slide visible={index == slideIndex} key={index}>
|
||||
<Image src={slide.src} alt="" role="presentation" />
|
||||
<Image src={slide.src} alt={t(`${slide.key}.imgAlt`)} />
|
||||
<TextAnimation visible={index == slideIndex}>
|
||||
<Heading>{t(`${slide.key}.title`)}</Heading>
|
||||
<Body>{t(`${slide.key}.body`)}</Body>
|
||||
{slide.isAvailableInBeta && (
|
||||
<LinkButton
|
||||
href={data?.transcript.form_beta_users}
|
||||
target="_blank"
|
||||
tooltip={t('beta.tooltip')}
|
||||
variant={'primary'}
|
||||
size={'sm'}
|
||||
style={{ marginTop: '1rem', width: 'fit-content' }}
|
||||
>
|
||||
{t('beta.text')}
|
||||
</LinkButton>
|
||||
)}
|
||||
</TextAnimation>
|
||||
</Slide>
|
||||
))}
|
||||
@@ -221,7 +241,7 @@ export const IntroSlider = () => {
|
||||
display: { base: 'none', xsm: 'block' },
|
||||
})}
|
||||
>
|
||||
{SLIDES.map((_, index) => (
|
||||
{filteredSlides.map((_, index) => (
|
||||
<Dot key={index} selected={index == slideIndex} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -71,11 +71,7 @@ export const LaterMeetingDialog = ({
|
||||
aria-label={t('copyUrl')}
|
||||
tooltip={t('copyUrl')}
|
||||
>
|
||||
{isRoomUrlCopied ? (
|
||||
<RiCheckLine aria-hidden="true" />
|
||||
) : (
|
||||
<RiFileCopyLine aria-hidden="true" />
|
||||
)}
|
||||
{isRoomUrlCopied ? <RiCheckLine /> : <RiFileCopyLine />}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -107,18 +103,13 @@ export const LaterMeetingDialog = ({
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<RiCheckLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||
{t('copied')}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<RiFileCopyLine
|
||||
style={{ marginRight: '6px', minWidth: '18px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t('copy')}
|
||||
</>
|
||||
@@ -140,11 +131,7 @@ export const LaterMeetingDialog = ({
|
||||
>
|
||||
{isCopied ? (
|
||||
<>
|
||||
<RiCheckLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<RiCheckLine size={18} style={{ marginRight: '8px' }} />
|
||||
{t('copied')}
|
||||
</>
|
||||
) : (
|
||||
@@ -152,7 +139,6 @@ export const LaterMeetingDialog = ({
|
||||
<RiFileCopyLine
|
||||
size={18}
|
||||
style={{ marginRight: '8px', minWidth: '18px' }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{isHovered ? (
|
||||
t('copy')
|
||||
@@ -187,7 +173,6 @@ export const LaterMeetingDialog = ({
|
||||
className={css({
|
||||
fill: 'primary.500',
|
||||
})}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<Text variant="sm" style={{ marginTop: '1rem' }}>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { RiAddLine, RiLink } from '@remixicon/react'
|
||||
import { LaterMeetingDialog } from '@/features/home/components/LaterMeetingDialog'
|
||||
import { IntroSlider } from '@/features/home/components/IntroSlider'
|
||||
import { MoreLink } from '@/features/home/components/MoreLink'
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { ReactNode, useState } from 'react'
|
||||
|
||||
import { css } from '@/styled-system/css'
|
||||
import { menuRecipe } from '@/primitives/menuRecipe.ts'
|
||||
@@ -19,7 +19,6 @@ import { usePersistentUserChoices } from '@/features/rooms/livekit/hooks/usePers
|
||||
import { useConfig } from '@/api/useConfig'
|
||||
import { LoginButton } from '@/components/LoginButton'
|
||||
import { ApiRoom } from '@/features/rooms/api/ApiRoom'
|
||||
import { LoadingScreen } from '@/components/LoadingScreen'
|
||||
|
||||
const Columns = ({ children }: { children?: ReactNode }) => {
|
||||
return (
|
||||
@@ -156,34 +155,9 @@ export const Home = () => {
|
||||
|
||||
const { mutateAsync: createRoom } = useCreateRoom()
|
||||
const [laterRoom, setLaterRoom] = useState<null | ApiRoom>(null)
|
||||
const [redirectFailed, setRedirectFailed] = useState(false)
|
||||
|
||||
const { data } = useConfig()
|
||||
|
||||
useEffect(() => {
|
||||
const checkSiteAndRedirect = async () => {
|
||||
if (!data?.external_home_url) return
|
||||
if (isLoggedIn === false) {
|
||||
try {
|
||||
await fetch(data.external_home_url, {
|
||||
method: 'HEAD', // Use HEAD to avoid downloading the full page
|
||||
mode: 'no-cors', // Needed for cross-origin requests
|
||||
})
|
||||
window.location.replace(data.external_home_url)
|
||||
} catch (error) {
|
||||
setRedirectFailed(true)
|
||||
console.error('Site is not reachable:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkSiteAndRedirect()
|
||||
}, [isLoggedIn, data])
|
||||
|
||||
if (data?.external_home_url && isLoggedIn == false && !redirectFailed) {
|
||||
return <LoadingScreen header={false} footer={false} delay={0} />
|
||||
}
|
||||
|
||||
return (
|
||||
<UserAware>
|
||||
<Screen>
|
||||
|
||||
@@ -25,27 +25,15 @@ export const TermsOfServiceRoute = () => {
|
||||
{/* Article 2 */}
|
||||
<H lvl={2}>{t('articles.article2.title')}</H>
|
||||
<P>{t('articles.article2.content')}</P>
|
||||
{ensureArray(
|
||||
t('articles.article2.paragraphs', {
|
||||
returnObjects: true,
|
||||
})
|
||||
).map((paragraph, index) => (
|
||||
<P key={index}>{paragraph}</P>
|
||||
))}
|
||||
<P>{t('articles.article2.purposes')}</P>
|
||||
|
||||
{/* Article 3 */}
|
||||
<H lvl={2}>{t('articles.article3.title')}</H>
|
||||
{ensureArray(
|
||||
t('articles.article3.paragraphs', {
|
||||
returnObjects: true,
|
||||
})
|
||||
).map((paragraph, index) => (
|
||||
<P key={index}>{paragraph}</P>
|
||||
))}
|
||||
<P>{t('articles.article3.definition')}</P>
|
||||
|
||||
{/* Article 4 */}
|
||||
<H lvl={2}>{t('articles.article4.title')}</H>
|
||||
<P>{t('articles.article4.definition')}</P>
|
||||
<P>{t('articles.article4.content')}</P>
|
||||
|
||||
{/* Article 5 */}
|
||||
<H lvl={2} margin={false}>
|
||||
@@ -74,7 +62,6 @@ export const TermsOfServiceRoute = () => {
|
||||
__html: t('articles.article5.sections.section1.paragraph3'),
|
||||
}}
|
||||
></P>
|
||||
<P>{t('articles.article5.sections.section1.paragraph4')}</P>
|
||||
|
||||
{/* Section 5.2 */}
|
||||
<H lvl={3} bold>
|
||||
@@ -124,12 +111,15 @@ export const TermsOfServiceRoute = () => {
|
||||
))}
|
||||
|
||||
{/* Article 7 */}
|
||||
<H lvl={2}>{t('articles.article7.title')}</H>
|
||||
<H lvl={2} margin={false}>
|
||||
{t('articles.article7.title')}
|
||||
</H>
|
||||
|
||||
{/* Section 7.1 */}
|
||||
<H lvl={3} bold>
|
||||
{t('articles.article7.sections.section1.title')}
|
||||
</H>
|
||||
<P>{t('articles.article7.sections.section1.content')}</P>
|
||||
{ensureArray(
|
||||
t('articles.article7.sections.section1.paragraphs', {
|
||||
returnObjects: true,
|
||||
@@ -142,51 +132,16 @@ export const TermsOfServiceRoute = () => {
|
||||
<H lvl={3} bold>
|
||||
{t('articles.article7.sections.section2.title')}
|
||||
</H>
|
||||
{ensureArray(
|
||||
t('articles.article7.sections.section2.paragraphs', {
|
||||
returnObjects: true,
|
||||
})
|
||||
).map((paragraph, index) => (
|
||||
<P key={index}>{paragraph}</P>
|
||||
))}
|
||||
|
||||
{/* Section 7.3 */}
|
||||
<H lvl={3} bold>
|
||||
{t('articles.article7.sections.section3.title')}
|
||||
</H>
|
||||
{ensureArray(
|
||||
t('articles.article7.sections.section3.paragraphs', {
|
||||
returnObjects: true,
|
||||
})
|
||||
).map((paragraph, index) => (
|
||||
<P key={index}>{paragraph}</P>
|
||||
))}
|
||||
|
||||
{/* Section 7.4 */}
|
||||
<H lvl={3} bold>
|
||||
{t('articles.article7.sections.section4.title')}
|
||||
</H>
|
||||
{ensureArray(
|
||||
t('articles.article7.sections.section4.paragraphs', {
|
||||
returnObjects: true,
|
||||
})
|
||||
).map((paragraph, index) => (
|
||||
<P key={index}>{paragraph}</P>
|
||||
))}
|
||||
|
||||
{/* Section 7.5 */}
|
||||
<H lvl={3} bold>
|
||||
{t('articles.article7.sections.section5.title')}
|
||||
</H>
|
||||
<P>
|
||||
{t('articles.article7.sections.section5.content')
|
||||
{t('articles.article7.sections.section2.content')
|
||||
.split('https://github.com/suitenumerique/meet')[0]
|
||||
.replace('https://github.com/suitenumerique/meet', '')}{' '}
|
||||
<A href="https://github.com/suitenumerique/meet" color="primary">
|
||||
https://github.com/suitenumerique/meet
|
||||
</A>
|
||||
{'. '}
|
||||
{
|
||||
t('articles.article7.sections.section5.content').split(
|
||||
t('articles.article7.sections.section2.content').split(
|
||||
'https://github.com/suitenumerique/meet'
|
||||
)[1]
|
||||
}
|
||||
|
||||
@@ -104,16 +104,6 @@ export const MainNotificationToast = () => {
|
||||
{ timeout: NotificationDuration.ALERT }
|
||||
)
|
||||
break
|
||||
case NotificationType.TranscriptionRequested:
|
||||
case NotificationType.ScreenRecordingRequested:
|
||||
toastQueue.add(
|
||||
{
|
||||
participant,
|
||||
type: notification.type,
|
||||
},
|
||||
{ timeout: NotificationDuration.RECORDING_REQUESTED }
|
||||
)
|
||||
break
|
||||
case NotificationType.PermissionsRemoved: {
|
||||
const removedSources = notification?.data?.removedSources
|
||||
if (!removedSources?.length) break
|
||||
|
||||
@@ -13,5 +13,4 @@ export const NotificationDuration = {
|
||||
LOWER_HAND: ToastDuration.EXTRA_LONG,
|
||||
RECORDING_SAVING: ToastDuration.EXTRA_LONG,
|
||||
REACTION_RECEIVED: ToastDuration.SHORT,
|
||||
RECORDING_REQUESTED: ToastDuration.LONG,
|
||||
} as const
|
||||
|
||||
@@ -9,10 +9,8 @@ export enum NotificationType {
|
||||
TranscriptionStarted = 'transcriptionStarted',
|
||||
TranscriptionStopped = 'transcriptionStopped',
|
||||
TranscriptionLimitReached = 'transcriptionLimitReached',
|
||||
TranscriptionRequested = 'transcriptionRequested',
|
||||
ScreenRecordingStarted = 'screenRecordingStarted',
|
||||
ScreenRecordingStopped = 'screenRecordingStopped',
|
||||
ScreenRecordingRequested = 'screenRecordingRequested',
|
||||
ScreenRecordingLimitReached = 'screenRecordingLimitReached',
|
||||
RecordingSaving = 'recordingSaving',
|
||||
PermissionsRemoved = 'permissionsRemoved',
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
import { useToast } from '@react-aria/toast'
|
||||
import { useMemo, useRef } from 'react'
|
||||
|
||||
import { StyledToastContainer, ToastProps } from './Toast'
|
||||
import { HStack } from '@/styled-system/jsx'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { NotificationType } from '../NotificationType'
|
||||
import { Button } from '@/primitives'
|
||||
import { css } from '@/styled-system/css'
|
||||
import { useSidePanel } from '@/features/rooms/livekit/hooks/useSidePanel'
|
||||
|
||||
export function ToastRecordingRequest({
|
||||
state,
|
||||
...props
|
||||
}: Readonly<ToastProps>) {
|
||||
const { t } = useTranslation('notifications')
|
||||
const ref = useRef(null)
|
||||
const { toastProps, contentProps } = useToast(props, state, ref)
|
||||
const participant = props.toast.content.participant
|
||||
const type = props.toast.content.type
|
||||
|
||||
const {
|
||||
isTranscriptOpen,
|
||||
openTranscript,
|
||||
isScreenRecordingOpen,
|
||||
openScreenRecording,
|
||||
} = useSidePanel()
|
||||
|
||||
const options = useMemo(() => {
|
||||
switch (type) {
|
||||
case NotificationType.TranscriptionRequested:
|
||||
return {
|
||||
key: 'transcript.requested',
|
||||
isMenuOpen: isTranscriptOpen,
|
||||
openMenu: openTranscript,
|
||||
}
|
||||
case NotificationType.ScreenRecordingRequested:
|
||||
return {
|
||||
key: 'screenRecording.requested',
|
||||
isMenuOpen: isScreenRecordingOpen,
|
||||
openMenu: openScreenRecording,
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}, [
|
||||
type,
|
||||
isTranscriptOpen,
|
||||
isScreenRecordingOpen,
|
||||
openTranscript,
|
||||
openScreenRecording,
|
||||
])
|
||||
|
||||
if (!options) return
|
||||
|
||||
return (
|
||||
<StyledToastContainer {...toastProps} ref={ref}>
|
||||
<HStack
|
||||
justify="center"
|
||||
alignItems="center"
|
||||
{...contentProps}
|
||||
padding={14}
|
||||
gap={0}
|
||||
>
|
||||
{t(options.key, {
|
||||
name: participant?.name,
|
||||
})}
|
||||
{!options.isMenuOpen && (
|
||||
<div
|
||||
className={css({
|
||||
marginLeft: '0.5rem',
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="text"
|
||||
className={css({
|
||||
color: 'primary.300',
|
||||
})}
|
||||
onPress={options.openMenu}
|
||||
>
|
||||
{t('openMenu')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</HStack>
|
||||
</StyledToastContainer>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import { ToastLowerHand } from './ToastLowerHand'
|
||||
import { ToastAnyRecording } from './ToastAnyRecording'
|
||||
import { ToastRecordingSaving } from './ToastRecordingSaving'
|
||||
import { ToastPermissionsRemoved } from './ToastPermissionsRemoved'
|
||||
import { ToastRecordingRequest } from './ToastRecordingRequest'
|
||||
|
||||
interface ToastRegionProps extends AriaToastRegionProps {
|
||||
state: ToastState<ToastData>
|
||||
@@ -53,12 +52,6 @@ const renderToast = (
|
||||
case NotificationType.ScreenRecordingLimitReached:
|
||||
return <ToastAnyRecording key={toast.key} toast={toast} state={state} />
|
||||
|
||||
case NotificationType.TranscriptionRequested:
|
||||
case NotificationType.ScreenRecordingRequested:
|
||||
return (
|
||||
<ToastRecordingRequest key={toast.key} toast={toast} state={state} />
|
||||
)
|
||||
|
||||
case NotificationType.RecordingSaving:
|
||||
return (
|
||||
<ToastRecordingSaving key={toast.key} toast={toast} state={state} />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user