Compare commits

..

3 Commits

Author SHA1 Message Date
lebaudantoine 3d7f2957f9 wip support experimental calendar api 2026-05-01 18:43:37 +02:00
lebaudantoine d83d3ccc18 insert link in mail 2026-05-01 17:43:18 +02:00
lebaudantoine 52a9a583e8 wip initiate dev environment 2026-05-01 17:12:53 +02:00
33 changed files with 4505 additions and 488 deletions
-11
View File
@@ -8,17 +8,6 @@ and this project adheres to
## [Unreleased] ## [Unreleased]
### Added
- 🔒️(backend) add validation of Room.configuration
- ✨(helm) add support multiple transcribe worker / endpoint #1247
### Fixed
- ♻(frontend) standardize role terminology across localizations
- 🐛(backend) make start-recording atomic and fault-tolerant
- 🔒️(frontend) room ids are generated with non-cryptographic rand
## [1.15.0] - 2026-04-30 ## [1.15.0] - 2026-04-30
### Added ### Added
+1 -1
View File
@@ -109,7 +109,7 @@ k8s_resource('meet-backend', resource_deps=['postgresql', 'minio', 'redis', 'liv
k8s_resource('meet-celery-backend', resource_deps=['redis']) k8s_resource('meet-celery-backend', resource_deps=['redis'])
k8s_resource('meet-celery-summarize', resource_deps=['redis']) k8s_resource('meet-celery-summarize', resource_deps=['redis'])
k8s_resource('meet-celery-summary-backend', resource_deps=['redis']) k8s_resource('meet-celery-summary-backend', resource_deps=['redis'])
k8s_resource('meet-celery-transcribe-default', resource_deps=['redis']) k8s_resource('meet-celery-transcribe', resource_deps=['redis'])
k8s_resource('meet-backend-migrate', resource_deps=['meet-backend']) k8s_resource('meet-backend-migrate', resource_deps=['meet-backend'])
k8s_resource('livekit-livekit-server', resource_deps=['redis']) k8s_resource('livekit-livekit-server', resource_deps=['redis'])
k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server']) k8s_resource('livekit-livekit-server-test-connection', resource_deps=['livekit-livekit-server'])
-7
View File
@@ -101,12 +101,6 @@ update_npm_version "mail"
# Update backend pyproject.toml # Update backend pyproject.toml
update_python_version "backend" update_python_version "backend"
# Run uv lock in backend
print_info "Running uv lock in backend..."
cd "src/backend"
uv lock
cd -
# Update summary pyproject.toml # Update summary pyproject.toml
update_python_version "summary" update_python_version "summary"
@@ -155,7 +149,6 @@ echo " - src/frontend/package.json"
echo " - src/sdk/package.json" echo " - src/sdk/package.json"
echo " - src/mail/package.json" echo " - src/mail/package.json"
echo " - src/backend/pyproject.toml" echo " - src/backend/pyproject.toml"
echo " - src/backend/uv.lock"
echo " - src/summary/pyproject.toml" echo " - src/summary/pyproject.toml"
echo " - src/agents/pyproject.toml" echo " - src/agents/pyproject.toml"
echo " - CHANGELOG.md" echo " - CHANGELOG.md"
+3 -4
View File
@@ -44,15 +44,14 @@ server {
add_header Pragma "no-cache" always; add_header Pragma "no-cache" always;
add_header Expires 0 always; add_header Expires 0 always;
set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com https://*.cloud.microsoft"; set $ms_domains "https://*.live.com https://*.office.com https://*.microsoft.com https://*.office365.com https://*.sharepoint.com";
set $ministries_domains "https://courrieljf.ccomptes.fr";
set $nonce $request_id; set $nonce $request_id;
set $csp "upgrade-insecure-requests; "; set $csp "upgrade-insecure-requests; ";
set $csp "${csp}frame-ancestors ${ms_domains} ${ministries_domains}; "; set $csp "${csp}frame-ancestors ${ms_domains}; ";
set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; "; set $csp "${csp}script-src 'nonce-${nonce}' 'strict-dynamic'; ";
set $csp "${csp}connect-src 'self' ${ms_domains} ${ministries_domains}; "; set $csp "${csp}connect-src 'self' ${ms_domains}; ";
set $csp "${csp}frame-src 'none'; "; set $csp "${csp}frame-src 'none'; ";
set $csp "${csp}object-src 'none'; "; set $csp "${csp}object-src 'none'; ";
set $csp "${csp}base-uri 'none'; "; set $csp "${csp}base-uri 'none'; ";
+27 -23
View File
@@ -36,7 +36,7 @@
"source-map-loader": "^5.0.0", "source-map-loader": "^5.0.0",
"webpack": "^5.95.0", "webpack": "^5.95.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "5.2.1" "webpack-dev-server": "5.1.0"
} }
}, },
"node_modules/@apidevtools/json-schema-ref-parser": { "node_modules/@apidevtools/json-schema-ref-parser": {
@@ -8996,6 +8996,23 @@
"safe-buffer": "~5.1.0" "safe-buffer": "~5.1.0"
} }
}, },
"node_modules/html-entities": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz",
"integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
],
"license": "MIT"
},
"node_modules/html-escaper": { "node_modules/html-escaper": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz",
@@ -15654,16 +15671,15 @@
} }
}, },
"node_modules/webpack-dev-server": { "node_modules/webpack-dev-server": {
"version": "5.2.1", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz", "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz",
"integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==", "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/bonjour": "^3.5.13", "@types/bonjour": "^3.5.13",
"@types/connect-history-api-fallback": "^1.5.4", "@types/connect-history-api-fallback": "^1.5.4",
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/express-serve-static-core": "^4.17.21",
"@types/serve-index": "^1.9.4", "@types/serve-index": "^1.9.4",
"@types/serve-static": "^1.15.5", "@types/serve-static": "^1.15.5",
"@types/sockjs": "^0.3.36", "@types/sockjs": "^0.3.36",
@@ -15674,9 +15690,10 @@
"colorette": "^2.0.10", "colorette": "^2.0.10",
"compression": "^1.7.4", "compression": "^1.7.4",
"connect-history-api-fallback": "^2.0.0", "connect-history-api-fallback": "^2.0.0",
"express": "^4.21.2", "express": "^4.19.2",
"graceful-fs": "^4.2.6", "graceful-fs": "^4.2.6",
"http-proxy-middleware": "^2.0.7", "html-entities": "^2.4.0",
"http-proxy-middleware": "^2.0.3",
"ipaddr.js": "^2.1.0", "ipaddr.js": "^2.1.0",
"launch-editor": "^2.6.1", "launch-editor": "^2.6.1",
"open": "^10.0.3", "open": "^10.0.3",
@@ -15711,19 +15728,6 @@
} }
} }
}, },
"node_modules/webpack-dev-server/node_modules/@types/express-serve-static-core": {
"version": "4.19.8",
"resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz",
"integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@types/qs": "*",
"@types/range-parser": "*",
"@types/send": "*"
}
},
"node_modules/webpack-dev-server/node_modules/define-lazy-prop": { "node_modules/webpack-dev-server/node_modules/define-lazy-prop": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz",
@@ -15767,9 +15771,9 @@
} }
}, },
"node_modules/webpack-dev-server/node_modules/ws": { "node_modules/webpack-dev-server/node_modules/ws": {
"version": "8.20.0", "version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
+1 -1
View File
@@ -53,7 +53,7 @@
"source-map-loader": "^5.0.0", "source-map-loader": "^5.0.0",
"webpack": "^5.95.0", "webpack": "^5.95.0",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "5.2.1" "webpack-dev-server": "5.1.0"
}, },
"prettier": "office-addin-prettier-config", "prettier": "office-addin-prettier-config",
"browserslist": [ "browserslist": [
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "thunderbird",
"version": "1.0.0",
"main": "index.js",
"private": true,
"scripts": {
"dev": "web-ext run",
"lint": "web-ext lint --source-dir=./src"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"web-ext": "^10.1.0"
}
}
+48
View File
@@ -0,0 +1,48 @@
import { buildMeetingMessage } from "./lib/meeting-message.js";
console.log("[meeting-link] background loaded at", new Date().toISOString());
(async () => {
try {
console.log("[meeting-link] browser.calendar exists?", !!browser.calendar);
if (browser.calendar) {
console.log("[meeting-link] calendar namespace keys:",
Object.keys(browser.calendar));
}
// Try the most basic read call — list configured calendars.
// The exact method name varies between experiment versions; we'll
// try the common one first.
if (browser.calendar.calendars?.query) {
const calendars = await browser.calendar.calendars.query({});
console.log("[meeting-link] calendars found:", calendars.length, calendars);
} else {
console.warn("[meeting-link] calendars.query not present — namespace shape:",
browser.calendar);
}
} catch (err) {
console.error("[meeting-link] calendar probe failed:", err);
}
})();
// Hardcoded for Spike 1. Spike 2 replaces this with a fetch() to your API.
const STUB_MEETING_DATA = {
url: "https://meet.example.com/m/abc-123-xyz",
telephony: {
phone_number: "+33123456789",
pin_code: "1234567890",
},
};
browser.runtime.onMessage.addListener(async (msg) => {
if (msg?.type === "GET_MEETING_MESSAGE") {
try {
const built = buildMeetingMessage(STUB_MEETING_DATA);
return { ok: true, ...built };
} catch (err) {
console.error("[meeting-link] build failed", err);
return { ok: false, error: String(err.message || err) };
}
}
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

+41
View File
@@ -0,0 +1,41 @@
{
"manifest_version": 3,
"name": "Meeting Link (dev)",
"version": "0.1.0",
"description": "Spike 0 — verifying the dev loop",
"browser_specific_settings": {
"gecko": {
"id": "meeting-link-dev@yourcompany.example",
"strict_min_version": "128.0"
}
},
"background": {
"scripts": ["background.js"],
"type": "module"
},
"compose_action": {
"default_title": "Insert meeting link",
"default_popup": "popup/popup.html",
"default_icon": "icons/icon-32.png"
},
"permissions": ["compose"],
"experiment_apis": {
"calendar_provider": {
"schema": "experiments/calendar/schema/calendar-provider.json",
"parent": {
"scopes": ["addon_parent"],
"paths": [["calendar", "provider"]],
"script": "experiments/calendar/parent/ext-calendar-provider.js",
"events": ["startup"]
}
},
"calendar_calendars": {
"schema": "experiments/calendar/schema/calendar-calendars.json",
"parent": {
"scopes": ["addon_parent"],
"paths": [["calendar", "calendars"]],
"script": "experiments/calendar/parent/ext-calendar-calendars.js"
}
}
}
}
@@ -0,0 +1,18 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Meeting link</title>
<style>
body { font: 13px system-ui; padding: 12px; min-width: 240px; margin: 0; }
button { font: inherit; padding: 6px 12px; cursor: pointer; }
#status { margin-top: 8px; color: #555; min-height: 1.2em; white-space: pre-wrap; }
#status.error { color: #b00020; }
</style>
</head>
<body>
<button id="insert">Insert meeting link</button>
<div id="status"></div>
<script src="popup.js"></script>
</body>
</html>
+65
View File
@@ -0,0 +1,65 @@
const insertBtn = document.getElementById("insert");
const statusEl = document.getElementById("status");
function setStatus(text, isError = false) {
statusEl.textContent = text;
statusEl.classList.toggle("error", isError);
}
insertBtn.addEventListener("click", async () => {
insertBtn.disabled = true;
setStatus("Generating link…");
try {
// 1. Find the compose tab this popup belongs to.
// A compose_action popup is anchored to a compose window, so the
// "active tab in the current window" is the compose tab itself.
const [composeTab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (!composeTab) throw new Error("No compose tab found");
// 2. Read the current compose state — we need to know if we're
// in HTML mode or plain-text mode, and we need the existing body
// so we can append rather than overwrite.
const details = await browser.compose.getComposeDetails(composeTab.id);
// 3. Ask the background to produce the meeting message.
const reply = await browser.runtime.sendMessage({
type: "GET_MEETING_MESSAGE",
});
if (!reply?.ok) throw new Error(reply?.error || "Background error");
// 4. Append to the existing body in the right format.
if (details.isPlainText) {
const newBody = (details.plainTextBody || "") + "\n\n" + reply.text;
await browser.compose.setComposeDetails(composeTab.id, {
plainTextBody: newBody,
});
} else {
const newBody = appendHtmlBeforeBodyEnd(details.body || "", reply.html);
await browser.compose.setComposeDetails(composeTab.id, {
body: newBody,
});
}
setStatus("Inserted ✓");
setTimeout(() => window.close(), 600);
} catch (err) {
console.error("[meeting-link] popup insert failed", err);
setStatus("Failed: " + (err.message || err), true);
insertBtn.disabled = false;
}
});
/**
* Append HTML right before </body>, or fall back to concatenation if no
* </body> tag is present (Thunderbird's compose body is usually a full
* HTML document, but be defensive).
*/
function appendHtmlBeforeBodyEnd(currentHtml, fragment) {
const idx = currentHtml.toLowerCase().lastIndexOf("</body>");
if (idx === -1) return currentHtml + fragment;
return currentHtml.slice(0, idx) + fragment + currentHtml.slice(idx);
}
+30
View File
@@ -0,0 +1,30 @@
// web-ext-config.cjs
const os = require("os");
const path = require("path");
function thunderbirdBinary() {
if (process.env.WEB_EXT_FIREFOX) return process.env.WEB_EXT_FIREFOX;
switch (process.platform) {
case "darwin":
return "/Applications/Thunderbird Beta.app/Contents/MacOS/thunderbird";
case "linux":
return "/usr/bin/thunderbird"; // adjust to your install
case "win32":
return "C:\\Program Files\\Mozilla Thunderbird\\thunderbird.exe";
default:
throw new Error("Unsupported platform: " + process.platform);
}
}
module.exports = {
sourceDir: "./src",
artifactsDir: "./web-ext-artifacts",
run: {
firefox: thunderbirdBinary(),
firefoxProfile: path.join(os.homedir(), ".thunderbird-dev-profile"),
profileCreateIfMissing: true,
keepProfileChanges: true,
browserConsole: true,
},
ignoreFiles: ["package-lock.json", "web-ext-config.cjs", "*.md"],
};
-27
View File
@@ -14,7 +14,6 @@ from django.utils.translation import gettext_lazy as _
from django_pydantic_field.rest_framework import SchemaField from django_pydantic_field.rest_framework import SchemaField
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from pydantic import ValidationError as PydanticValidationError
from rest_framework import serializers from rest_framework import serializers
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from timezone_field.rest_framework import TimeZoneSerializerField from timezone_field.rest_framework import TimeZoneSerializerField
@@ -132,16 +131,6 @@ class RoomSerializer(serializers.ModelSerializer):
fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"] fields = ["id", "name", "slug", "configuration", "access_level", "pin_code"]
read_only_fields = ["id", "slug", "pin_code"] read_only_fields = ["id", "slug", "pin_code"]
def validate_configuration(self, value):
"""Validate room configuration against the RoomConfiguration schema."""
if value is None or value == {}:
return value
try:
RoomConfiguration.model_validate(value)
except PydanticValidationError as e:
raise serializers.ValidationError(e.errors()) from e
return value
def to_representation(self, instance): def to_representation(self, instance):
""" """
Add users only for administrator users. Add users only for administrator users.
@@ -317,22 +306,6 @@ class MuteParticipantSerializer(BaseParticipantsManagementSerializer):
) )
RoomConfigurationTrackSource = Literal[
"camera", "microphone", "screen_share", "screen_share_audio"
]
class RoomConfiguration(BaseModel):
"""Validate room configuration structure.
Unknown fields are rejected.
"""
can_publish_sources: list[RoomConfigurationTrackSource] | None = None
model_config = {"extra": "forbid"}
TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"] TrackSource = Literal["SCREEN_SHARE", "SCREEN_SHARE_AUDIO", "CAMERA", "MICROPHONE"]
+10 -26
View File
@@ -6,9 +6,7 @@ from logging import getLogger
from urllib.parse import unquote, urlparse from urllib.parse import unquote, urlparse
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError
from django.core.files.storage import default_storage from django.core.files.storage import default_storage
from django.db import IntegrityError, transaction
from django.db.models import Q from django.db.models import Q
from django.http import Http404 from django.http import Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -322,27 +320,16 @@ class RoomViewSet(
options = serializer.validated_data.get("options") options = serializer.validated_data.get("options")
room = self.get_object() room = self.get_object()
try: # May raise exception if an active or initiated recording already exist for the room
with transaction.atomic(): recording = models.Recording.objects.create(
recording = models.Recording.objects.create( room=room,
room=room, mode=mode,
mode=mode, options=options.model_dump(exclude_none=True) if options else {},
options=options.model_dump(exclude_none=True) if options else {}, )
)
models.RecordingAccess.objects.create(
user=self.request.user,
role=models.RoleChoices.OWNER,
recording=recording,
)
except (DjangoValidationError, IntegrityError): models.RecordingAccess.objects.create(
# DjangoValidationError covers the Python-level check (full_clean); user=self.request.user, role=models.RoleChoices.OWNER, recording=recording
# IntegrityError covers the race where two concurrent requests both )
# pass that check and the DB-level UNIQUE constraint catches the loser.
return drf_response.Response(
{"error": f"A recording is already in progress for room {room.slug}"},
status=drf_status.HTTP_409_CONFLICT,
)
worker_service = get_worker_service(mode=recording.mode) worker_service = get_worker_service(mode=recording.mode)
worker_manager = WorkerServiceMediator(worker_service=worker_service) worker_manager = WorkerServiceMediator(worker_service=worker_service)
@@ -350,12 +337,9 @@ class RoomViewSet(
try: try:
worker_manager.start(recording) worker_manager.start(recording)
except RecordingStartError: except RecordingStartError:
models.Recording.objects.filter(pk=recording.pk).update(
status=models.RecordingStatusChoices.FAILED_TO_START
)
return drf_response.Response( return drf_response.Response(
{"error": f"Recording failed to start for room {room.slug}"}, {"error": f"Recording failed to start for room {room.slug}"},
status=drf_status.HTTP_502_BAD_GATEWAY, status=drf_status.HTTP_500_INTERNAL_SERVER_ERROR,
) )
if settings.METADATA_COLLECTOR_ENABLED and ( if settings.METADATA_COLLECTOR_ENABLED and (
+1 -1
View File
@@ -123,7 +123,7 @@ class LobbyService:
def request_entry( def request_entry(
self, self,
room: models.Room, room,
request, request,
username: str, username: str,
) -> Tuple[LobbyParticipant, Optional[Dict]]: ) -> Tuple[LobbyParticipant, Optional[Dict]]:
@@ -117,7 +117,7 @@ def test_api_files_create_file_authenticated_success():
policy_parsed = urlparse(policy) policy_parsed = urlparse(policy)
assert policy_parsed.scheme == "http" assert policy_parsed.scheme == "http"
assert policy_parsed.netloc in ["minio:9000", "localhost:9000"] assert policy_parsed.netloc == "localhost:9000"
assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png" assert policy_parsed.path == f"/meet-media-storage/files/{file.id!s}.png"
query_params = parse_qs(policy_parsed.query) query_params = parse_qs(policy_parsed.query)
@@ -232,7 +232,7 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
""" """
room = RoomFactory( room = RoomFactory(
access_level=RoomAccessLevel.PUBLIC, access_level=RoomAccessLevel.PUBLIC,
configuration={"can_publish_sources": ["camera"]}, configuration={"can_publish_sources": ["mock-source"]},
) )
user = UserFactory() user = UserFactory()
@@ -264,7 +264,7 @@ def test_api_rooms_retrieve_authenticated_public(mock_token):
user=user, user=user,
username=None, username=None,
color=None, color=None,
sources=["camera"], sources=["mock-source"],
is_admin_or_owner=False, is_admin_or_owner=False,
participant_id=None, participant_id=None,
) )
@@ -363,7 +363,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
other_user = UserFactory() other_user = UserFactory()
room = RoomFactory( room = RoomFactory(
configuration={"can_publish_sources": ["camera"]}, configuration={"can_publish_sources": ["mock-source"]},
) )
UserResourceAccessFactory(resource=room, user=user, role="member") UserResourceAccessFactory(resource=room, user=user, role="member")
UserResourceAccessFactory(resource=room, user=other_user, role="member") UserResourceAccessFactory(resource=room, user=other_user, role="member")
@@ -401,7 +401,7 @@ def test_api_rooms_retrieve_members(mock_token, django_assert_num_queries, setti
user=user, user=user,
username=None, username=None,
color=None, color=None,
sources=["camera"], sources=["mock-source"],
is_admin_or_owner=False, is_admin_or_owner=False,
participant_id=None, participant_id=None,
) )
@@ -140,18 +140,16 @@ def test_start_recording_worker_error(
mock_worker_service_factory.assert_called_once_with(mode="screen_recording") mock_worker_service_factory.assert_called_once_with(mode="screen_recording")
assert response.status_code == 502 assert response.status_code == 500
assert response.json() == { assert response.json() == {
"error": f"Recording failed to start for room {room.slug}" "error": f"Recording failed to start for room {room.slug}"
} }
# Recording object should be created even if worker fails, and moved out # Recording object should be created even if worker fails
# of the unique-constraint window so the room is not locked.
assert Recording.objects.count() == 1 assert Recording.objects.count() == 1
recording = Recording.objects.first() recording = Recording.objects.first()
assert recording.room == room assert recording.room == room
assert recording.mode == "screen_recording" assert recording.mode == "screen_recording"
assert recording.status == "failed_to_start"
# Verify recording access details # Verify recording access details
assert recording.accesses.count() == 1 assert recording.accesses.count() == 1
@@ -160,72 +158,6 @@ def test_start_recording_worker_error(
assert access.role == "owner" assert access.role == "owner"
@pytest.mark.parametrize(
"status",
["active", "initiated"],
)
def test_start_recording_conflict_when_already_in_progress(
status, mock_worker_service_factory, mock_worker_manager, settings
):
"""Should return 409 when a second start is attempted while a recording is already active."""
settings.RECORDING_ENABLE = True
room = RoomFactory()
user = UserFactory()
room.accesses.create(user=user, role="owner")
# Pre-existing active recording for the same room.
Recording.objects.create(room=room, mode="screen_recording", status="active")
client = APIClient()
client.force_login(user)
response = client.post(
f"/api/v1.0/rooms/{room.id}/start-recording/",
{"mode": "screen_recording"},
)
assert response.status_code == 409
assert response.json() == {
"error": f"A recording is already in progress for room {room.slug}"
}
# No new recording row, no access row leaked from the rolled-back transaction.
assert Recording.objects.count() == 1
assert Recording.objects.first().accesses.count() == 0
mock_worker_manager.start.assert_not_called()
def test_start_recording_after_worker_failure_unblocks_room(
mock_worker_service_factory, mock_worker_manager, settings
):
"""Should allow a new recording when the previous recording failed."""
settings.RECORDING_ENABLE = True
room = RoomFactory()
user = UserFactory()
room.accesses.create(user=user, role="owner")
mock_worker_manager.start = mock.Mock(
side_effect=[RecordingStartError("boom"), None]
)
client = APIClient()
client.force_login(user)
first = client.post(
f"/api/v1.0/rooms/{room.id}/start-recording/",
{"mode": "screen_recording"},
)
assert first.status_code == 502
second = client.post(
f"/api/v1.0/rooms/{room.id}/start-recording/",
{"mode": "screen_recording"},
)
assert second.status_code == 201
assert Recording.objects.count() == 2
def test_start_recording_success( def test_start_recording_success(
mock_worker_service_factory, mock_worker_manager, settings mock_worker_service_factory, mock_worker_manager, settings
): ):
@@ -67,7 +67,7 @@ def test_api_rooms_update_members():
"name": "New name", "name": "New name",
"slug": "should-be-ignored", "slug": "should-be-ignored",
"access_level": RoomAccessLevel.RESTRICTED, "access_level": RoomAccessLevel.RESTRICTED,
"configuration": {"can_publish_sources": ["camera", "microphone"]}, "configuration": {"the_key": "the_value"},
}, },
format="json", format="json",
) )
@@ -95,7 +95,7 @@ def test_api_rooms_update_administrators():
"name": "New name", "name": "New name",
"slug": "should-be-ignored", "slug": "should-be-ignored",
"access_level": RoomAccessLevel.PUBLIC, "access_level": RoomAccessLevel.PUBLIC,
"configuration": {"can_publish_sources": ["camera", "microphone"]}, "configuration": {"the_key": "the_value"},
}, },
format="json", format="json",
) )
@@ -104,98 +104,7 @@ def test_api_rooms_update_administrators():
assert room.name == "New name" assert room.name == "New name"
assert room.slug == "new-name" assert room.slug == "new-name"
assert room.access_level == RoomAccessLevel.PUBLIC assert room.access_level == RoomAccessLevel.PUBLIC
assert room.configuration == {"can_publish_sources": ["camera", "microphone"]} assert room.configuration == {"the_key": "the_value"}
@pytest.mark.parametrize(
"configuration",
[
{},
{"can_publish_sources": ["camera", "microphone"]},
{
"can_publish_sources": [
"camera",
"microphone",
"screen_share",
"screen_share_audio",
]
},
{"can_publish_sources": []},
{"can_publish_sources": None},
],
)
def test_api_rooms_update_configuration_valid(configuration):
"""Administrators should be allowed to set valid configurations."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{"configuration": configuration},
format="json",
)
assert response.status_code == 200
room.refresh_from_db()
assert room.configuration == configuration
def test_api_rooms_update_configuration_extra_keys_rejected():
"""Extra keys in configuration should be rejected."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{
"configuration": {
"can_publish_sources": ["camera"],
"arbitrary_key": "value",
}
},
format="json",
)
assert response.status_code == 400
room.refresh_from_db()
assert room.configuration == {}
@pytest.mark.parametrize("invalid_source", ["invalid_source", "CAMERA"])
def test_api_rooms_update_configuration_invalid_source_value(invalid_source):
"""Invalid source values should be rejected."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{"configuration": {"can_publish_sources": [invalid_source]}},
format="json",
)
assert response.status_code == 400
room.refresh_from_db()
assert room.configuration == {}
def test_api_rooms_update_configuration_wrong_type():
"""Configuration values with wrong types should be rejected."""
user = UserFactory()
room = RoomFactory(users=[(user, "owner")])
client = APIClient()
client.force_login(user)
response = client.patch(
f"/api/v1.0/rooms/{room.id!s}/",
{"configuration": {"can_publish_sources": "camera"}},
format="json",
)
assert response.status_code == 400
room.refresh_from_db()
assert room.configuration == {}
def test_api_rooms_update_administrators_of_another(): def test_api_rooms_update_administrators_of_another():
+4 -120
View File
@@ -54,7 +54,7 @@
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20", "eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.10", "postcss": "8.5.6",
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "5.8.3", "typescript": "5.8.3",
"vite": "7.3.2", "vite": "7.3.2",
@@ -1759,35 +1759,6 @@
"ts-pattern": "5.9.0" "ts-pattern": "5.9.0"
} }
}, },
"node_modules/@pandacss/core/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/dev": { "node_modules/@pandacss/dev": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/@pandacss/dev/-/dev-1.8.2.tgz", "resolved": "https://registry.npmjs.org/@pandacss/dev/-/dev-1.8.2.tgz",
@@ -1845,35 +1816,6 @@
"ts-pattern": "5.9.0" "ts-pattern": "5.9.0"
} }
}, },
"node_modules/@pandacss/generator/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/is-valid-prop": { "node_modules/@pandacss/is-valid-prop": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.8.2.tgz", "resolved": "https://registry.npmjs.org/@pandacss/is-valid-prop/-/is-valid-prop-1.8.2.tgz",
@@ -1975,35 +1917,6 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/@pandacss/node/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/node/node_modules/prettier": { "node_modules/@pandacss/node/node_modules/prettier": {
"version": "3.2.5", "version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
@@ -2063,35 +1976,6 @@
"postcss": "8.5.6" "postcss": "8.5.6"
} }
}, },
"node_modules/@pandacss/postcss/node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/@pandacss/preset-base": { "node_modules/@pandacss/preset-base": {
"version": "1.8.2", "version": "1.8.2",
"resolved": "https://registry.npmjs.org/@pandacss/preset-base/-/preset-base-1.8.2.tgz", "resolved": "https://registry.npmjs.org/@pandacss/preset-base/-/preset-base-1.8.2.tgz",
@@ -9548,9 +9432,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.10", "version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
+1 -1
View File
@@ -59,7 +59,7 @@
"eslint-plugin-jsx-a11y": "6.10.2", "eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react-hooks": "5.2.0", "eslint-plugin-react-hooks": "5.2.0",
"eslint-plugin-react-refresh": "0.4.20", "eslint-plugin-react-refresh": "0.4.20",
"postcss": "8.5.10", "postcss": "8.5.6",
"prettier": "3.8.1", "prettier": "3.8.1",
"typescript": "5.8.3", "typescript": "5.8.3",
"vite": "7.3.2", "vite": "7.3.2",
@@ -1,20 +1,10 @@
// Google Meet uses only letters in a room identifier // Google Meet uses only letters in a room identifier
const ROOM_ID_ALLOWED_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz' const ROOM_ID_ALLOWED_CHARACTERS = 'abcdefghijklmnopqrstuvwxyz'
const getRandomChar = () => { const getRandomChar = () =>
const maxValue = ROOM_ID_ALLOWED_CHARACTERS[
Math.floor(0x100000000 / ROOM_ID_ALLOWED_CHARACTERS.length) * Math.floor(Math.random() * ROOM_ID_ALLOWED_CHARACTERS.length)
ROOM_ID_ALLOWED_CHARACTERS.length
const randomValue = new Uint32Array(1)
do {
crypto.getRandomValues(randomValue)
} while (randomValue[0] >= maxValue)
return ROOM_ID_ALLOWED_CHARACTERS[
randomValue[0] % ROOM_ID_ALLOWED_CHARACTERS.length
] ]
}
const generateSegment = (length: number): string => const generateSegment = (length: number): string =>
Array.from(Array(length), getRandomChar).join('') Array.from(Array(length), getRandomChar).join('')
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"error": { "error": {
"title": "Aufzeichnung nicht verfügbar", "title": "Aufzeichnung nicht verfügbar",
"body": "Die Aufzeichnung ist nicht verfügbar oder wurde möglicherweise gelöscht. Nur der Host der Besprechung hat Zugriff darauf. Wende dich bei Bedarf gerne an die Person." "body": "Die Aufzeichnung ist nicht verfügbar oder wurde möglicherweise gelöscht. Nur die*der Organisator:in der Besprechung hat Zugriff darauf. Wende dich bei Bedarf gerne an die Person."
}, },
"expired": { "expired": {
"title": "Aufzeichnung abgelaufen", "title": "Aufzeichnung abgelaufen",
@@ -22,7 +22,7 @@
"button": "Herunterladen", "button": "Herunterladen",
"warning": { "warning": {
"title": "Linkfreigabe deaktiviert", "title": "Linkfreigabe deaktiviert",
"body": "Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Hosts können sie herunterladen." "body": "Die Freigabe der Aufzeichnung per Link ist noch nicht verfügbar. Nur Organisator:innen können sie herunterladen."
} }
} }
} }
+4 -4
View File
@@ -3,7 +3,7 @@
"heading": { "heading": {
"normal": "Du hast das Meeting verlassen", "normal": "Du hast das Meeting verlassen",
"duplicateIdentity": "Du bist dem Meeting von einem anderen Gerät aus beigetreten", "duplicateIdentity": "Du bist dem Meeting von einem anderen Gerät aus beigetreten",
"participantRemoved": "Du wurdest vom Host aus dem Meeting entfernt" "participantRemoved": "Du wurdest von der*dem Organisator:in aus dem Meeting entfernt"
}, },
"home": "Zur Startseite zurückkehren", "home": "Zur Startseite zurückkehren",
"back": "Dem Meeting erneut beitreten" "back": "Dem Meeting erneut beitreten"
@@ -383,7 +383,7 @@
"body": "Transkribiere bis zu {{max_duration}} Meetingdauer.", "body": "Transkribiere bis zu {{max_duration}} Meetingdauer.",
"bodyWithoutMaxDuration": "Transkribieren dein Meeting ohne Zeitbegrenzung.", "bodyWithoutMaxDuration": "Transkribieren dein Meeting ohne Zeitbegrenzung.",
"details": { "details": {
"receiver": "Das Transkript wird an den Host und die Co-Hosts gesendet.", "receiver": "Das Transkript wird an die*den Organisator:in und die Mitorganisierenden gesendet.",
"destination": "Ein neues Dokument wird erstellt auf", "destination": "Ein neues Dokument wird erstellt auf",
"destinationUnknown": "Ein neues Dokument wird erstellt", "destinationUnknown": "Ein neues Dokument wird erstellt",
"language": "Meeting-Sprache:", "language": "Meeting-Sprache:",
@@ -438,7 +438,7 @@
"linkMore": "Dokumentation öffnen", "linkMore": "Dokumentation öffnen",
"linkAriaLabel": "Dokumentation zur Aufzeichnung öffnen öffnet in neuem Tab", "linkAriaLabel": "Dokumentation zur Aufzeichnung öffnen öffnet in neuem Tab",
"details": { "details": {
"receiver": "Die Aufzeichnung wird an den Host und die Co-Hosts gesendet.", "receiver": "Die Aufzeichnung wird an die*den Organisator:in und die Mitorganisierenden gesendet.",
"destination": "Diese Aufzeichnung wird vorübergehend auf unseren Servern gespeichert", "destination": "Diese Aufzeichnung wird vorübergehend auf unseren Servern gespeichert",
"loading": "Aufzeichnung wird gestartet", "loading": "Aufzeichnung wird gestartet",
"linkMore": "Mehr erfahren", "linkMore": "Mehr erfahren",
@@ -494,7 +494,7 @@
"button": "OK" "button": "OK"
}, },
"admin": { "admin": {
"description": "Diese Host-Einstellungen ermöglichen dir die Kontrolle über dein Meeting. Nur Hosts haben Zugriff auf diese Optionen.", "description": "Diese Einstellungen für Organisierende ermöglichen dir die Kontrolle über dein Meeting. Nur Organisierende haben Zugriff auf diese Optionen.",
"access": { "access": {
"title": "Raumzugang", "title": "Raumzugang",
"description": "Diese Einstellungen gelten auch für zukünftige Meetings in diesem Raum.", "description": "Diese Einstellungen gelten auch für zukünftige Meetings in diesem Raum.",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"error": { "error": {
"title": "Recording unavailable", "title": "Recording unavailable",
"body": "The recording is unavailable or may have been deleted. Only the meeting host can access it. Feel free to contact them if needed." "body": "The recording is unavailable or may have been deleted. Only the meeting organizer can access it. Feel free to contact them if needed."
}, },
"expired": { "expired": {
"title": "Recording expired", "title": "Recording expired",
@@ -22,7 +22,7 @@
"button": "Download", "button": "Download",
"warning": { "warning": {
"title": "Link sharing disabled", "title": "Link sharing disabled",
"body": "Sharing the recording via link is not yet available. Only hosts can download it." "body": "Sharing the recording via link is not yet available. Only organizers can download it."
} }
} }
} }
+4 -4
View File
@@ -3,7 +3,7 @@
"heading": { "heading": {
"normal": "You have left the meeting", "normal": "You have left the meeting",
"duplicateIdentity": "You have joined the meeting from another device", "duplicateIdentity": "You have joined the meeting from another device",
"participantRemoved": "You have been removed from the meeting by a host" "participantRemoved": "You have been removed from the meeting by an administrator"
}, },
"home": "Return to home", "home": "Return to home",
"back": "Rejoin the meeting" "back": "Rejoin the meeting"
@@ -328,7 +328,7 @@
"chat": "Messages in the chat", "chat": "Messages in the chat",
"transcript": "Transcribe", "transcript": "Transcribe",
"screenRecording": "Record", "screenRecording": "Record",
"admin": "Host settings", "admin": "Admin settings",
"tools": "More tools", "tools": "More tools",
"info": "Meeting information" "info": "Meeting information"
}, },
@@ -338,7 +338,7 @@
"chat": "messages", "chat": "messages",
"transcript": "transcribe", "transcript": "transcribe",
"screenRecording": "record", "screenRecording": "record",
"admin": "host settings", "admin": "admin settings",
"tools": "more tools", "tools": "more tools",
"info": "meeting information" "info": "meeting information"
}, },
@@ -493,7 +493,7 @@
"button": "OK" "button": "OK"
}, },
"admin": { "admin": {
"description": "These host settings allow you to maintain control of your meeting. Only hosts can access these controls.", "description": "These organizer settings allow you to maintain control of your meeting. Only organizers can access these controls.",
"access": { "access": {
"title": "Room access", "title": "Room access",
"description": "These settings will also apply to future occurrences of this meeting.", "description": "These settings will also apply to future occurrences of this meeting.",
+2 -2
View File
@@ -1,7 +1,7 @@
{ {
"error": { "error": {
"title": "Opname niet beschikbaar", "title": "Opname niet beschikbaar",
"body": "De opname is niet beschikbaar of is mogelijk verwijderd. Alleen de host van de vergadering heeft toegang. Neem gerust contact met hem of haar op als dat nodig is." "body": "De opname is niet beschikbaar of is mogelijk verwijderd. Alleen de organisator van de vergadering heeft toegang. Neem gerust contact met hem of haar op als dat nodig is."
}, },
"expired": { "expired": {
"title": "Opname verlopen", "title": "Opname verlopen",
@@ -22,7 +22,7 @@
"button": "Downloaden", "button": "Downloaden",
"warning": { "warning": {
"title": "Delen via link uitgeschakeld", "title": "Delen via link uitgeschakeld",
"body": "Het delen van de opname via een link is nog niet beschikbaar. Alleen hosts kunnen deze downloaden." "body": "Het delen van de opname via een link is nog niet beschikbaar. Alleen organisatoren kunnen deze downloaden."
} }
} }
} }
+2 -2
View File
@@ -437,7 +437,7 @@
"linkMore": "Documentatie openen", "linkMore": "Documentatie openen",
"linkAriaLabel": "Documentatie over opname openen - opent in nieuw venster", "linkAriaLabel": "Documentatie over opname openen - opent in nieuw venster",
"details": { "details": {
"receiver": "De opname wordt verzonden naar de host en co-hosts.", "receiver": "De opname wordt verzonden naar de organisator en co-organisatoren.",
"destination": "Deze opname wordt tijdelijk op onze servers bewaard", "destination": "Deze opname wordt tijdelijk op onze servers bewaard",
"loading": "Opname wordt gestart", "loading": "Opname wordt gestart",
"linkMore": "Meer informatie", "linkMore": "Meer informatie",
@@ -493,7 +493,7 @@
"button": "Opnieuw proberen" "button": "Opnieuw proberen"
}, },
"admin": { "admin": {
"description": "Deze hostinstellingen geven u controle over uw vergadering. Alleen hosts hebben toegang tot deze bedieningselementen.", "description": "Deze organisatorinstellingen geven u controle over uw vergadering. Alleen organisatoren hebben toegang tot deze bedieningselementen.",
"access": { "access": {
"title": "Toegang tot vergadering", "title": "Toegang tot vergadering",
"description": "Deze instellingen zijn ook van toepassing op toekomstige sessies van deze vergadering.", "description": "Deze instellingen zijn ook van toepassing op toekomstige sessies van deze vergadering.",
@@ -1,62 +1,52 @@
{{- $sharedEnvVars := default (dict) .Values.celeryTranscribe.envVars -}} {{- $envVars := include "meet.common.env" (list . .Values.celeryTranscribe) -}}
{{- $fullName := include "meet.celeryTranscribe.fullname" . -}}
{{- $component := "celery-transcribe" -}} {{- $component := "celery-transcribe" -}}
{{- range $idx, $instance := .Values.celeryTranscribe.instances }}
{{- $instanceValues := merge $instance $.Values.celeryTranscribe }}
{{- $fullName := printf "%s-%s" (include "meet.celeryTranscribe.fullname" $) $instance.name }}
{{- $extraInstanceEnvVars := default (dict) $instance.extraEnvVars -}}
{{- $mergedInstanceEnvVars := merge $extraInstanceEnvVars $sharedEnvVars -}}
{{- $fakeInstanceObjectForEnvHelper := dict "envVars" $mergedInstanceEnvVars -}}
{{- $envVars := include "meet.common.env" (list . $fakeInstanceObjectForEnvHelper) -}}
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: {{ $fullName }} name: {{ $fullName }}
annotations: annotations:
{{- with $instanceValues.dpAnnotations }} {{- with .Values.celeryTranscribe.dpAnnotations }}
{{- toYaml . | nindent 4 }} {{- toYaml . | nindent 4 }}
{{- end }} {{- end }}
namespace: {{ $.Release.Namespace | quote }} namespace: {{ .Release.Namespace | quote }}
labels: labels:
{{- include "meet.common.labels" (list $ $component) | nindent 4 }} {{- include "meet.common.labels" (list . $component) | nindent 4 }}
instance: {{ $instance.name }}
spec: spec:
replicas: {{ $instanceValues.replicas }} replicas: {{ .Values.celeryTranscribe.replicas }}
selector: selector:
matchLabels: matchLabels:
{{- include "meet.common.selectorLabels" (list $ $component) | nindent 6 }} {{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
instance: {{ $instance.name }}
template: template:
metadata: metadata:
annotations: annotations:
{{- with $instanceValues.podAnnotations }} {{- with .Values.celeryTranscribe.podAnnotations }}
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
labels: labels:
{{- include "meet.common.selectorLabels" (list $ $component) | nindent 8 }} {{- include "meet.common.selectorLabels" (list . $component) | nindent 8 }}
instance: {{ $instance.name }}
spec: spec:
{{- if $.Values.image.credentials }} {{- if $.Values.image.credentials }}
imagePullSecrets: imagePullSecrets:
- name: {{ include "meet.secret.dockerconfigjson.name" (dict "fullname" (include "meet.fullname" $) "imageCredentials" $.Values.image.credentials) }} - name: {{ include "meet.secret.dockerconfigjson.name" (dict "fullname" (include "meet.fullname" .) "imageCredentials" $.Values.image.credentials) }}
{{- end }} {{- end }}
shareProcessNamespace: {{ $instanceValues.shareProcessNamespace }} shareProcessNamespace: {{ .Values.celeryTranscribe.shareProcessNamespace }}
{{- with $instanceValues.podSecurityContext }} {{- with .Values.celeryTranscribe.podSecurityContext }}
securityContext: securityContext:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
containers: containers:
{{- with $instanceValues.sidecars }} {{- with .Values.celeryTranscribe.sidecars }}
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
- name: {{ $.Chart.Name }} - name: {{ .Chart.Name }}
image: "{{ ($instanceValues.image | default dict).repository | default $.Values.image.repository }}:{{ ($instanceValues.image | default dict).tag | default $.Values.image.tag }}" image: "{{ (.Values.celeryTranscribe.image | default dict).repository | default .Values.image.repository }}:{{ (.Values.celeryTranscribe.image | default dict).tag | default .Values.image.tag }}"
imagePullPolicy: {{ ($instanceValues.image | default dict).pullPolicy | default $.Values.image.pullPolicy }} imagePullPolicy: {{ (.Values.celeryTranscribe.image | default dict).pullPolicy | default .Values.image.pullPolicy }}
{{- with $instanceValues.command }} {{- with .Values.celeryTranscribe.command }}
command: command:
{{- toYaml . | nindent 12 }} {{- toYaml . | nindent 12 }}
{{- end }} {{- end }}
{{- with $instanceValues.args }} {{- with .Values.celeryTranscribe.args }}
args: args:
{{- toYaml . | nindent 12 }} {{- toYaml . | nindent 12 }}
{{- end }} {{- end }}
@@ -64,65 +54,65 @@ spec:
{{- if $envVars }} {{- if $envVars }}
{{- $envVars | indent 12 }} {{- $envVars | indent 12 }}
{{- end }} {{- end }}
{{- with $instanceValues.securityContext }} {{- with .Values.celeryTranscribe.securityContext }}
securityContext: securityContext:
{{- toYaml . | nindent 12 }} {{- toYaml . | nindent 12 }}
{{- end }} {{- end }}
ports: ports:
- name: http - name: http
containerPort: {{ $instanceValues.service.targetPort }} containerPort: {{ .Values.celeryTranscribe.service.targetPort }}
protocol: TCP protocol: TCP
{{- if $instanceValues.probes.liveness }} {{- if .Values.celeryTranscribe.probes.liveness }}
livenessProbe: livenessProbe:
{{- include "meet.probes.abstract" (merge $instanceValues.probes.liveness (dict "targetPort" $instanceValues.service.targetPort)) | nindent 12 }} {{- include "meet.probes.abstract" (merge .Values.celeryTranscribe.probes.liveness (dict "targetPort" .Values.celeryTranscribe.service.targetPort )) | nindent 12 }}
{{- end }} {{- end }}
{{- if $instanceValues.probes.readiness }} {{- if .Values.celeryTranscribe.probes.readiness }}
readinessProbe: readinessProbe:
{{- include "meet.probes.abstract" (merge $instanceValues.probes.readiness (dict "targetPort" $instanceValues.service.targetPort)) | nindent 12 }} {{- include "meet.probes.abstract" (merge .Values.celeryTranscribe.probes.readiness (dict "targetPort" .Values.celeryTranscribe.service.targetPort )) | nindent 12 }}
{{- end }} {{- end }}
{{- if $instanceValues.probes.startup }} {{- if .Values.celeryTranscribe.probes.startup }}
startupProbe: startupProbe:
{{- include "meet.probes.abstract" (merge $instanceValues.probes.startup (dict "targetPort" $instanceValues.service.targetPort)) | nindent 12 }} {{- include "meet.probes.abstract" (merge .Values.celeryTranscribe.probes.startup (dict "targetPort" .Values.celeryTranscribe.service.targetPort )) | nindent 12 }}
{{- end }} {{- end }}
{{- with $instanceValues.resources }} {{- with .Values.celeryTranscribe.resources }}
resources: resources:
{{- toYaml . | nindent 12 }} {{- toYaml . | nindent 12 }}
{{- end }} {{- end }}
volumeMounts: volumeMounts:
{{- range $index, $value := $.Values.mountFiles }} {{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}" - name: "files-{{ $index }}"
mountPath: {{ $value.path }} mountPath: {{ $value.path }}
subPath: content subPath: content
{{- end }} {{- end }}
{{- range $name, $volume := $instanceValues.persistence }} {{- range $name, $volume := .Values.celeryTranscribe.persistence }}
- name: "{{ $name }}" - name: "{{ $name }}"
mountPath: "{{ $volume.mountPath }}" mountPath: "{{ $volume.mountPath }}"
{{- end }} {{- end }}
{{- range $instanceValues.extraVolumeMounts }} {{- range .Values.celeryTranscribe.extraVolumeMounts }}
- name: {{ .name }} - name: {{ .name }}
mountPath: {{ .mountPath }} mountPath: {{ .mountPath }}
subPath: {{ .subPath | default "" }} subPath: {{ .subPath | default "" }}
readOnly: {{ .readOnly }} readOnly: {{ .readOnly }}
{{- end }} {{- end }}
{{- with $instanceValues.nodeSelector }} {{- with .Values.celeryTranscribe.nodeSelector }}
nodeSelector: nodeSelector:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{- with $instanceValues.affinity }} {{- with .Values.celeryTranscribe.affinity }}
affinity: affinity:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
{{- with $instanceValues.tolerations }} {{- with .Values.celeryTranscribe.tolerations }}
tolerations: tolerations:
{{- toYaml . | nindent 8 }} {{- toYaml . | nindent 8 }}
{{- end }} {{- end }}
volumes: volumes:
{{- range $index, $value := $.Values.mountFiles }} {{- range $index, $value := .Values.mountFiles }}
- name: "files-{{ $index }}" - name: "files-{{ $index }}"
configMap: configMap:
name: "{{ include "meet.fullname" $ }}-files-{{ $index }}" name: "{{ include "meet.fullname" $ }}-files-{{ $index }}"
{{- end }} {{- end }}
{{- range $name, $volume := $instanceValues.persistence }} {{- range $name, $volume := .Values.celeryTranscribe.persistence }}
- name: "{{ $name }}" - name: "{{ $name }}"
{{- if eq $volume.type "emptyDir" }} {{- if eq $volume.type "emptyDir" }}
emptyDir: {} emptyDir: {}
@@ -131,7 +121,7 @@ spec:
claimName: "{{ $fullName }}-{{ $name }}" claimName: "{{ $fullName }}-{{ $name }}"
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- range $instanceValues.extraVolumes }} {{- range .Values.celeryTranscribe.extraVolumes }}
- name: {{ .name }} - name: {{ .name }}
{{- if .existingClaim }} {{- if .existingClaim }}
persistentVolumeClaim: persistentVolumeClaim:
@@ -153,18 +143,15 @@ spec:
{{- end }} {{- end }}
{{- end }} {{- end }}
--- ---
{{ if $instanceValues.pdb.enabled }} {{ if .Values.celeryTranscribe.pdb.enabled }}
apiVersion: policy/v1 apiVersion: policy/v1
kind: PodDisruptionBudget kind: PodDisruptionBudget
metadata: metadata:
name: {{ $fullName }} name: {{ $fullName }}
namespace: {{ $.Release.Namespace | quote }} namespace: {{ .Release.Namespace | quote }}
spec: spec:
maxUnavailable: 1 maxUnavailable: 1
selector: selector:
matchLabels: matchLabels:
{{- include "meet.common.selectorLabels" (list $ $component) | nindent 6 }} {{- include "meet.common.selectorLabels" (list . $component) | nindent 6 }}
instance: {{ $instance.name }}
---
{{ end }} {{ end }}
{{- end }}
-15
View File
@@ -848,21 +848,6 @@ celeryTranscribe:
pdb: pdb:
enabled: false enabled: false
## @param celeryTranscribe.instances List of celeryTranscribe instances to deploy. Each entry creates a dedicated Deployment. Useful when wanting to use multiple instances of WhisperX (configure each endpoint in the extraEnv value specific to that instance).
## @extra celeryTranscribe.instances[].name Unique name suffix for the instance (used in the Deployment name and pod labels)
## @extra celeryTranscribe.instances[].replicas Override the number of replicas for this specific instance
## @extra celeryTranscribe.instances[].extraEnvVars Additional environment variables for this specific instance (same structure as envVars)
## @extra celeryTranscribe.instances[].command Override the container command for this specific instance
## @extra celeryTranscribe.instances[].args Override the container args for this specific instance
## @extra celeryTranscribe.instances[].resources Override resource requirements for this specific instance
## @extra celeryTranscribe.instances[].nodeSelector Override node selector for this specific instance
## @extra celeryTranscribe.instances[].affinity Override affinity for this specific instance
## @extra celeryTranscribe.instances[].tolerations Override tolerations for this specific instance
## @extra celeryTranscribe.instances[].pdb.enabled Enable pdb for this specific instance
instances:
- name: default
extraEnvVars: {}
## @section celerySummarize ## @section celerySummarize
celerySummarize: celerySummarize:
+1 -1
View File
@@ -20,7 +20,7 @@ dependencies = [
[project.optional-dependencies] [project.optional-dependencies]
dev = [ dev = [
"ruff==0.15.6", "ruff==0.15.6",
"pytest==9.0.3", "pytest==9.0.2",
"responses>=0.25.8", "responses>=0.25.8",
] ]