diff --git a/apps/course/serializers/online.py b/apps/course/serializers/online.py index 44d7396..031b937 100644 --- a/apps/course/serializers/online.py +++ b/apps/course/serializers/online.py @@ -24,7 +24,6 @@ class OnlineClassTokenVerifySerializer(serializers.Serializer): class LiveSessionRoomCreateSerializer(serializers.Serializer): room_id = serializers.CharField(required=False, max_length=255, allow_blank=True) subject = serializers.CharField(required=False, max_length=255, allow_blank=True) - metadata = serializers.DictField(required=False) def validate_room_id(self, value: str) -> str: return value.strip() diff --git a/apps/course/services/plugnmeet.py b/apps/course/services/plugnmeet.py index 5a3180b..fd8c179 100644 --- a/apps/course/services/plugnmeet.py +++ b/apps/course/services/plugnmeet.py @@ -27,10 +27,14 @@ class PlugNMeetClient: raise ImproperlyConfigured("PlugNMeet integration settings are incomplete.") def create_room(self, payload: Dict[str, Any]) -> Dict[str, Any]: - return self._post("/auth/room/create", payload) + # Convert entire payload keys to camelCase as required by PlugNMeet protocol + prepared = self._camelize_dict(payload) + return self._post("/auth/room/create", prepared) def get_join_token(self, payload: Dict[str, Any]) -> Dict[str, Any]: - return self._post("/auth/room/getJoinToken", payload) + # Convert entire payload keys to camelCase as required by PlugNMeet protocol + prepared = self._camelize_dict(payload) + return self._post("/auth/room/getJoinToken", prepared) def is_room_active(self, room_id: str) -> Dict[str, Any]: return self._post("/auth/room/isRoomActive", {"roomId": room_id}) @@ -71,6 +75,20 @@ class PlugNMeetClient: return data + @staticmethod + def _snake_to_camel(key: str) -> str: + parts = key.split("_") + if not parts: + return key + return parts[0] + "".join(p.capitalize() or "" for p in parts[1:]) + + def _camelize_dict(self, obj: Any) -> Any: + if isinstance(obj, dict): + return {self._snake_to_camel(k): self._camelize_dict(v) for k, v in obj.items()} + if isinstance(obj, list): + return [self._camelize_dict(v) for v in obj] + return obj + def _build_signature(self, body: str) -> str: digest = hmac.new(self.api_secret.encode("utf-8"), body.encode("utf-8"), hashlib.sha256) return digest.hexdigest() diff --git a/apps/course/views/live_session.py b/apps/course/views/live_session.py index a0be3c0..4bd5535 100644 --- a/apps/course/views/live_session.py +++ b/apps/course/views/live_session.py @@ -23,8 +23,13 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView): def post(self, request, slug, *args, **kwargs): logger.info(f"[LiveSession Create] Request from user_id={request.user.id} for course={slug}") - - serializer = self.get_serializer(data=request.data or {}) + + data = dict(request.data or {}) + if 'metadata' in data: + logger.warning("[LiveSession Create] 'metadata' provided by client will be ignored for security reasons.") + data.pop('metadata', None) + + serializer = self.get_serializer(data=data) serializer.is_valid(raise_exception=True) course = get_object_or_404(Course, slug=slug) @@ -37,7 +42,7 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView): subject = serializer.validated_data.get('subject') or f"{course.title} Live Session" room_id = serializer.validated_data.get('room_id') or self._build_room_id(course) - metadata = self._merge_metadata(subject, serializer.validated_data.get('metadata') or {}) + metadata = self._build_metadata(subject) payload = { 'room_id': room_id, @@ -103,22 +108,56 @@ class CourseLiveSessionRoomCreateAPIView(GenericAPIView): timestamp = timezone.now().strftime('%Y%m%d%H%M%S') return f"{course.slug}-{timestamp}" - def _merge_metadata(self, subject: str, overrides: dict) -> dict: - base = { + def _build_metadata(self, subject: str) -> dict: + # Build secured, centralized metadata. Client overrides are NOT allowed. + return { 'room_title': subject, 'default_lock_settings': { 'lock_microphone': True, 'lock_webcam': True, 'lock_screen_sharing': True, + 'lock_whiteboard': False, + 'lock_shared_notepad': False, + 'lock_chat': False, + 'lock_chat_send_message': False, + 'lock_chat_file_share': False, + 'lock_private_chat': False, }, 'room_features': { + 'allow_webcams': True, 'mute_on_start': True, + 'allow_screen_sharing': True, + 'allow_recording': True, + 'allow_rtmp': False, + 'allow_view_other_webcams': True, + 'allow_view_other_participants_list': True, + 'admin_only_webcams': False, + 'allow_polls': True, + 'room_duration': 0, + 'chat_features': { + 'allow_chat': True, + 'allow_file_upload': True, + }, + 'shared_note_pad_features': { + 'allowed_shared_note_pad': True, + }, + 'whiteboard_features': { + 'allowed_whiteboard': True, + }, + 'breakout_room_features': { + 'is_allow': True, + 'allowed_number_rooms': 6, + }, 'waiting_room_features': { 'is_active': False, }, + 'recording_features': { + 'is_allow': True, + 'is_allow_cloud': True, + 'enable_auto_cloud_recording': False, + }, }, } - return self._deep_update(base, overrides) def _deep_update(self, base: dict, overrides: dict) -> dict: for key, value in overrides.items(): @@ -188,6 +227,12 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): 'lock_microphone': True, 'lock_screen_sharing': True, 'lock_webcam': True, + 'lock_whiteboard': False, + 'lock_shared_notepad': False, + 'lock_chat': False, + 'lock_chat_send_message': False, + 'lock_chat_file_share': False, + 'lock_private_chat': False, } else: user_metadata['is_hidden'] = False diff --git a/docs/live-session-api.md b/docs/live-session-api.md index 2fd40fc..3b6790d 100644 --- a/docs/live-session-api.md +++ b/docs/live-session-api.md @@ -1,219 +1,114 @@ -# راهنمای اتصال فرانت‌اند به API لایو کلاس +# راهنمای گرفتن توکن و ورود کلاینت به کلاس‌های plugNmeet -این مستند جریان کامل «ایجاد روم لایو»، «گرفتن توکن ورود» و «مدیریت ورود استاد و دانشجو» را توضیح می‌دهد. +این راهنما خلاصه می‌کند که برای سناریوی استاد/دانشجو چگونه از سرویس plugNmeet توکن بگیریم و کلاینت فرانت‌اند (`client/`) با آن وارد کلاس شود. -## ۱. اعتبارسنجی وضعیت دوره +## پیش‌نیازها +- آدرس سرویس: `window.PLUG_N_MEET_SERVER_URL = "https://meet.newhorizonco.uk"` (در `config.js`). +- `api_key` و `secret` از فایل پیکربندی بک‌اند (`services/plugnmeet-server/config.yaml`). +- بدنهٔ درخواست‌ها باید با پروتکل JSON متناظر با پیام‌های پروتوباف (`plugnmeet-protocol`) ارسال شود؛ سرور طبق `HandleAuthHeaderCheck` هدرهای امنیتی را بررسی می‌کند. +## گام ۱: ایجاد یا فعال بودن اتاق + +### API Endpoint برای Django Backend: ``` -GET /api/courses//online/validate/ -Headers: - Authorization: Token +POST /api/courses//online/room/create/ ``` -### پاسخ نمونه (استاد، کلاس آنلاین در حال اجرا) - +### بدنه درخواست از فرانت به Django: ```json { - "course": { - "id": 42, - "slug": "algebra-10", - "title": "کلاس جبر", - "is_online": true, - "online_link": "https://imamjavad.app/courses/algebra-10/live", - "status": "ongoing", - "professor": { - "id": 10, - "fullname": "استاد نمونه", - "slug": "ostad-nemoone", - "avatar": "https://imamjavad.app/media/users/avatars/2025/10/ostad.jpg" - } - }, - "user": { - "id": 10, - "email": "prof@example.com", - "fullname": "استاد نمونه", - "avatar": "https://imamjavad.app/media/users/avatars/2025/10/ostad.jpg", - "roles": ["professor"], - "is_staff": false - }, - "metadata": { - "status": "ongoing", - "has_started": true, - "has_finished": false, - "professor_in_class": false, - "can_create_live_session": false, - "can_join_live_session": true, - "scheduled_times": { - "day": "monday", - "start_time": "09:00", - "timezone": "Asia/Tehran" - }, - "generated_at": "2025-10-14T01:32:45+03:30", - "validated_at": "2025-10-14T01:33:10+03:30", - "redirect_path": null, - "is_online": true, - "active_room_id": "algebra-1402", - "livesession_started_at": "2025-10-14T01:15:00+03:30", - "livesession_ended_at": null, - "live_session": { - "id": 7, - "room_id": "algebra-1402", - "subject": "کلاس جبر فصل ۱", - "started_at": "2025-10-14T01:15:00+03:30", - "ended_at": null - } - } + "subject": "کلاس جبر فصل ۱" // اختیاری - عنوان روم } ``` -- `can_create_live_session`: اگر `true` → استاد می‌تواند روم جدید بسازد (فقط وقتی کلاس آفلاین است) -- `can_join_live_session`: اگر `true` → کاربر می‌تواند به کلاس فعال بپیوندد (استاد یا دانشجو) -- `active_room_id`: room_id کلاس فعال (برای نمایش در UI) -- `livesession_started_at`: زمان شروع - برای محاسبه مدت سپری‌شده +**⚠️ نکات مهم:** +- **فرانت نباید `metadata` ارسال کند!** +- بک‌اند Django (در `apps/course/views/live_session.py`) به‌طور خودکار تنظیمات امنیتی را اعمال می‌کند +- این تضمین می‌کند که تنظیمات امنیتی به‌صورت متمرکز و یکسان اعمال شود -### پاسخ نمونه (استاد، کلاس آفلاین) +**🎯 تنظیمات ضروری برای نمایش فیچرها:** +- برای نمایش **Whiteboard**: باید `whiteboardFeatures.allowedWhiteboard: true` باشد +- برای نمایش **SharedNotePad**: باید `sharedNotePadFeatures.allowedSharedNotePad: true` باشد و Etherpad service فعال باشد +- برای نمایش **BreakoutRoom**: باید `breakoutRoomFeatures.isAllow: true` باشد (فقط در منوی admin) -```json -{ - "course": { "id": 42, "title": "کلاس جبر" }, - "user": { "id": 10, "fullname": "استاد نمونه" }, - "metadata": { - "status": "ongoing", - "has_started": true, - "has_finished": false, - "professor_in_class": false, - "can_create_live_session": true, - "can_join_live_session": false, - "scheduled_times": { "day": "monday", "time": "09:00" }, - "generated_at": "2025-10-14T01:32:45+03:30", - "validated_at": "2025-10-14T01:33:10+03:30", - "redirect_path": null, - "is_online": false, - "active_room_id": null, - "livesession_started_at": null, - "livesession_ended_at": null, - "live_session": null - } -} -``` +### بدنه درخواست از Django به PlugNMeet (خودکار): +بک‌اند Django این بدنه را خودش به PlugNMeet ارسال می‌کند: -### پاسخ نمونه (دانشجو ثبت‌نام‌کرده، کلاس آنلاین) +**⚠️ توجه به نامگذاری:** +- در Python می‌توانید از `snake_case` استفاده کنید +- اما **حتماً قبل از ارسال به PlugNMeet API** باید به `camelCase` تبدیل شود +- مثال: `default_lock_settings` → `defaultLockSettings` +- مثال: `room_features` → `roomFeatures` ```json { - "course": { "id": 42, "title": "کلاس جبر" }, - "user": { "id": 27, "fullname": "دانشجو نمونه" }, + "room_id": "algebra-1402", "metadata": { - "status": "ongoing", - "has_started": true, - "has_finished": false, - "professor_in_class": false, - "can_create_live_session": false, - "can_join_live_session": true, - "scheduled_times": { "day": "monday", "time": "09:00" }, - "generated_at": "2025-10-14T01:32:45+03:30", - "validated_at": "2025-10-14T01:33:10+03:30", - "redirect_path": null, - "is_online": true, - "active_room_id": "algebra-1402", - "livesession_started_at": "2025-10-14T01:15:00+03:30", - "livesession_ended_at": null, - "live_session": { - "id": 7, - "room_id": "algebra-1402", - "subject": "کلاس جبر فصل ۱", - "started_at": "2025-10-14T01:15:00+03:30", - "ended_at": null + "room_title": "کلاس جبر فصل ۱", + "defaultLockSettings": { + "lockMicrophone": true, // 🔒 قفل - فقط میزبان می‌تواند باز کند + "lockWebcam": true, // 🔒 قفل - فقط میزبان می‌تواند باز کند + "lockScreenSharing": true, // 🔒 قفل - فقط میزبان می‌تواند باز کند + "lockWhiteboard": false, // ✅ همه می‌توانند ویرایش کنند + "lockSharedNotepad": false, // ✅ همه می‌توانند ویرایش کنند + "lockChat": false, + "lockChatSendMessage": false, + "lockChatFileShare": false, + "lockPrivateChat": false + }, + "roomFeatures": { + "allowWebcams": true, + "muteOnStart": true, // 🔇 همه با میک خاموش وارد می‌شوند + "allowScreenSharing": true, + "allowRecording": true, + "allowRtmp": false, + "allowViewOtherWebcams": true, + "allowViewOtherParticipantsList": true, + "adminOnlyWebcams": false, + "allowPolls": true, + "roomDuration": 0, + "chatFeatures": { + "allowChat": true, + "allowFileUpload": true + }, + "sharedNotePadFeatures": { + "allowedSharedNotePad": true + }, + "whiteboardFeatures": { + "allowedWhiteboard": true + }, + "breakoutRoomFeatures": { + "isAllow": true, + "allowedNumberRooms": 6 + }, + "waitingRoomFeatures": { + "isActive": false + }, + "recordingFeatures": { + "isAllow": true, + "isAllowCloud": true, + "enableAutoCloudRecording": false + } } } } ``` -### پاسخ نمونه (دانشجو ثبت‌نام‌کرده، کلاس آفلاین) - -```json -{ - "course": { "id": 42, "title": "کلاس جبر" }, - "user": { "id": 27, "fullname": "دانشجو نمونه" }, - "metadata": { - "status": "ongoing", - "has_started": true, - "has_finished": false, - "professor_in_class": false, - "can_create_live_session": false, - "can_join_live_session": false, - "scheduled_times": { "day": "monday", "time": "09:00" }, - "generated_at": "2025-10-14T01:32:45+03:30", - "validated_at": "2025-10-14T01:33:10+03:30", - "redirect_path": null, - "is_online": false, - "active_room_id": null, - "livesession_started_at": null, - "livesession_ended_at": null, - "live_session": null - } -} -``` +> **چرا بک‌اند این کار را می‌کند؟** +> - ✅ **امنیت متمرکز**: تنظیمات امنیتی در یک جا کنترل می‌شود +> - ✅ **جلوگیری از دستکاری**: فرانت نمی‌تواند تنظیمات را تغییر دهد +> - ✅ **یکپارچگی**: همه کلاس‌ها با تنظیمات یکسان ساخته می‌شوند +> - 🔒 طبق تابع `AssignLockSettingsToUser` در `pkg/models/user_lock.go` این مقادیر برای کاربران غیر-admin اعمال می‌شود -### پاسخ نمونه (کاربر بدون دسترسی) - -```json -{ - "status": "error", - "code": "app_api_error", - "status_code": 403, - "message": "An error occurred while processing the request.", - "errors": [ - { "message": "You do not have access to this course." } - ] -} -``` - -## ۲. ساخت یا فعال کردن روم (استاد) - -``` -POST /api/courses//online/room/create/ -Headers: - Authorization: Token - Content-Type: application/json +## گام ۲: گرفتن توکن ورود -Body (نمونه): -{ - "subject": "کلاس جبر فصل ۱" // اختیاری؛ پیش‌فرض عنوان دوره + "Live Session" -} +### API Endpoint برای Django Backend: ``` - -**⚠️ نکات مهم:** -- **فرانت نباید `metadata` ارسال کند!** -- بک‌اند به‌طور خودکار تنظیمات امنیتی را اعمال می‌کند: - - `lock_microphone: true` - میکروفون برای همه قفل است - - `lock_webcam: true` - وبکم برای همه قفل است - - `lock_screen_sharing: true` - اشتراک صفحه برای همه قفل است - - `mute_on_start: true` - همه با میکروفون خاموش وارد می‌شوند -- **فقط میزبان (استاد)** می‌تواند این محدودیت‌ها را برداشته و به دانشجو اجازه دهد - -### پاسخ موفق (۲۰۱ یا ۲۰۰) - -```json -{ - "session": { - "id": 7, - "room_id": "algebra-1402", - "subject": "کلاس جبر فصل ۱", - "started_at": "2025-10-14T01:32:45+03:30" - }, - "plugnmeet": { - "status": "success", - "room_id": "algebra-1402", - "...": "پاسخ کامل PlugNMeet" - } -} +POST /api/courses/online/room/token/ ``` -## ۳. گرفتن توکن ورود به روم - +### درخواست از فرانت به Django: ``` -POST /api/courses/online/room/token/ Headers: Authorization: Token Content-Type: application/json @@ -226,22 +121,122 @@ Body: **⚠️ نکات مهم:** - **فرانت فقط `course_slug` ارسال می‌کند!** -- بک‌اند از `Authorization` header کاربر را شناسایی می‌کند +- بک‌اند Django از `Authorization` header کاربر را شناسایی می‌کند - بک‌اند خودش live session فعال دوره را پیدا می‌کند: ```python + # 1. پیدا کردن دوره course = Course.objects.get(slug=course_slug) - session = CourseLiveSession.objects.get(course=course, ended_at__isnull=True) + + # 2. پیدا کردن live session فعال + session = CourseLiveSession.objects.get( + course=course, + ended_at__isnull=True # session هایی که هنوز به پایان نرسیده‌اند + ) + + # 3. گرفتن room_id room_id = session.room_id ``` - بک‌اند خودش همه اطلاعات کاربر را می‌سازد: - - `user_id` از `request.user.id` + - `user_id` از `request.user` - `name` از `user.get_full_name()` یا `user.email` - - `is_admin` از `user.can_manage_course(course)` - تشخیص خودکار استاد/دانشجو + - `is_admin` از `user.can_manage_course(course)` - `profilePic` از `user.avatar` - - `lock_settings` خودکار برای دانشجو (همه قفل) + - `lock_settings` برای غیر-admin + +### بدنه درخواست از Django به PlugNMeet (خودکار): + +بک‌اند Django این payload را خودش می‌سازد و به PlugNMeet می‌فرستد: + +**برای استاد:** +```json +{ + "room_id": "algebra-1402", + "user_info": { + "user_id": "10", // 🔐 از request.user + "name": "استاد نمونه", // 🔐 از user.get_full_name() + "is_admin": true, // 🔐 از user.can_manage_course() + "user_metadata": { + "is_hidden": false, + "profilePic": "https://..." // 🔐 از user.avatar + } + } +} +``` + +**برای دانشجو:** +```json +{ + "room_id": "algebra-1402", + "user_info": { + "user_id": "27", // 🔐 از request.user + "name": "دانشجو نمونه", // 🔐 از user.get_full_name() + "is_admin": false, // 🔐 از user.can_manage_course() + "user_metadata": { + "profilePic": "https://...", // 🔐 از user.avatar + "lock_settings": { // 🔒 خودکار برای غیر-admin + "lock_microphone": true, + "lock_screen_sharing": true, + "lock_webcam": true, + "lock_whiteboard": false, // ✅ می‌تواند روی whiteboard بنویسد + "lock_shared_notepad": false, // ✅ می‌تواند در notepad بنویسد + "lock_chat": false, + "lock_chat_send_message": false, + "lock_chat_file_share": false, + "lock_private_chat": false + } + } + } +} +``` + +### نحوه کار بک‌اند Django: +```python +# 1. شناسایی کاربر از token +user = request.user # از Authorization header + +# 2. پیدا کردن دوره و session فعال +course = Course.objects.get(slug=course_slug) +session = CourseLiveSession.objects.get(course=course, ended_at__isnull=True) +room_id = session.room_id + +# 3. تشخیص نقش +is_admin = user.can_manage_course(course) # استاد یا مالک دوره + +# 4. ساخت user_info +user_info = { + 'user_id': str(user.id), + 'name': user.get_full_name() or user.email, + 'is_admin': is_admin, +} + +# 4. اضافه کردن profilePic +profile_pic = request.build_absolute_uri(user.avatar.url) +user_metadata['profilePic'] = profile_pic + +# 5. اضافه کردن lock_settings برای غیر-admin +if not is_admin: + user_metadata['lock_settings'] = { + 'lock_microphone': True, + 'lock_screen_sharing': True, + 'lock_webcam': True, + 'lock_whiteboard': False, # دانشجو می‌تواند روی whiteboard بنویسد + 'lock_shared_notepad': False, # دانشجو می‌تواند در notepad بنویسد + 'lock_chat': False, + 'lock_chat_send_message': False, + 'lock_chat_file_share': False, + 'lock_private_chat': False, + } +``` -### پاسخ موفق +### ارسال به PlugNMeet: +بک‌اند Django با هدرهای امنیتی به PlugNMeet ارسال می‌کند: +- `API-KEY`: از settings +- `HASH-SIGNATURE`: `HMAC_SHA256(body, secret)` +- این توکن JWT اختصاصی plugNmeet است که در `GeneratePNMJoinToken` ساخته می‌شود +- `is_admin: true` باعث می‌شود در `GetPNMJoinToken` کاربر به عنوان presenter با تمام دسترسی‌ها ثبت شود +- `lock_settings` باعث می‌شود در فرانت‌اند PlugNMeet دکمه‌های میکروفون/وبکم غیرفعال شوند +### پاسخ Django به فرانت: ```json { "room_id": "algebra-1402", @@ -249,85 +244,229 @@ Body: "plugnmeet": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "expires": 300, - "...": "پاسخ کامل PlugNMeet" + ... } } ``` -### نحوه استفاده: -- `token` باید در URL سرویس PlugNMeet استفاده شود: +فرانت با این `token` می‌تواند کاربر را به PlugNMeet وارد کند: +``` +https://meet.newhorizonco.uk/?access_token= +``` + +## گام ۳: ورود کلاینت با توکن +۱. توکن را در URL یا کوکی قرار دهید؛ کلاینت مقدار را از `access_token` در کوئری‌استرینگ یا از کوکی `pnm_access_token` می‌خواند (`getAccessToken` در `client/src/helpers/utils.ts`). +۲. آدرس ورود: `https://meet.newhorizonco.uk/?access_token=`. +۳. اپلیکیشن React موجود در `client/src/components/app/index.tsx` پس از بارگذاری: + - درخواست `POST /api/verifyToken` را با هدر `Authorization: ` می‌فرستد (`HandleVerifyToken`). + - اگر توکن معتبر باشد، لیست آدرس‌های NATS و موضوعات لازم را می‌گیرد و اتصال را آغاز می‌کند (`startNatsConn`). +۴. پس از اتصال، وضعیت کاربر و اتاق در Redux ذخیره می‌شود (`sessionSlice`). اگر کاربر ادمین باشد، تمام امکانات بدون محدودیت فعال است؛ در غیر این صورت مقدارهای `lock_settings` تعیین می‌کنند چه دکمه‌هایی فعال باشند. + +## کنترل حالت صحبت/شنیدن برای استاد و دانشجو + +### استاد (Moderator/Host): +- ✅ در توکن `is_admin: true` ارسال می‌شود +- ✅ بک‌اند Django در `apps/course/views/live_session.py` این را تشخیص می‌دهد: + ```python + is_admin = user.can_manage_course(course) # استاد یا مالک دوره ``` - https://meet.newhorizonco.uk/?access_token= +- ✅ سرور PlugNMeet در `GetPNMJoinToken` رول presenter را فعال می‌کند +- ✅ **هیچ قفلی** روی میکروفون، وبکم یا اشتراک صفحه اعمال نمی‌شود +- 🎤 استاد می‌تواند بلافاصله صحبت کند و به دانشجو **اجازه صحبت** دهد + +### دانشجو (Participant): +- 🔒 در توکن `is_admin: false` ارسال می‌شود +- 🔒 بک‌اند Django خودکار lock_settings را اضافه می‌کند: + ```python + if not is_admin: + user_metadata['lock_settings'] = { + 'lock_microphone': True, + 'lock_screen_sharing': True, + 'lock_webcam': True, + 'lock_whiteboard': False, # می‌تواند روی whiteboard بنویسد + 'lock_shared_notepad': False, # می‌تواند در notepad بنویسد + 'lock_chat': False, + 'lock_chat_send_message': False, + 'lock_chat_file_share': False, + 'lock_private_chat': False, + } ``` -- بک‌اند خودکار تشخیص می‌دهد: - - **استاد**: `is_admin: true` → همه دسترسی‌ها بدون محدودیت - - **دانشجو**: `is_admin: false` + `lock_settings` → میکروفون، وبکم و اشتراک صفحه قفل است +- 🔇 دکمه‌های میکروفون، وبکم و اشتراک صفحه **غیرفعال** هستند +- 👂 فقط می‌تواند **گوش دهد** تا میزبان اجازه دهد +- ✅ اما می‌تواند در **Whiteboard** و **SharedNotePad** بنویسد و چت کند +- این منطق در `joinModal.tsx` با متغیر `isMicLock` پیاده‌سازی شده است + +### نحوه دادن اجازه به دانشجو: +- میزبان باید از داخل کلاس از طریق UI کنترل کند +- یا از API `/api/updateLockSettings` یا `switchPresenter` استفاده کند + +## نکات تکمیلی + +### توکن‌ها و انقضا: +- توکن‌ها زمان انقضای مفهومی دارند (`client.token_validity` در YAML) +- در صورت نزدیک شدن به انقضا، کلاینت خودکار با `REQ_RENEW_PNM_TOKEN` درخواست تمدید می‌دهد + +### Authorization: +- برای درخواست‌های بعدی به `/api/...` همان هدر `Authorization` را ست کنید +- کلاینت این کار را در `helpers/api/plugNmeetAPI.ts` انجام می‌دهد + +### مدیریت دسترسی‌ها: +- اگر می‌خواهید دانشجو را به صحبت‌کننده ارتقا دهید: `/api/updateLockSettings` یا `switchPresenter` +- این کار فقط توسط **میزبان** امکان‌پذیر است + +## 🔐 جمع‌بندی امنیت + +### ❌ چیزهایی که فرانت نباید انجام دهد: + +#### موقع ساخت روم: +- ❌ ارسال `metadata` +- ❌ ارسال `default_lock_settings` +- ❌ ارسال `room_features` + +#### موقع گرفتن توکن: +- ❌ ارسال `room_id` (بک‌اند خودش از session فعال می‌گیرد) +- ❌ ارسال `user_info` +- ❌ ارسال `is_admin` +- ❌ ارسال `lock_settings` +- ❌ ارسال `user_id` یا `name` + +### ✅ چیزهایی که فرانت فقط ارسال می‌کند: + +#### موقع ساخت روم: +```json +{ + "room_id": "algebra-1402", // اختیاری + "subject": "کلاس جبر" // اختیاری +} +``` + +#### موقع گرفتن توکن: +```json +{ + "course_slug": "algebra-10" // فقط این! +} +``` ++ `Authorization: Token ` در header + +### ✅ چیزهایی که بک‌اند Django خودش انجام می‌دهد: + +#### برای همه درخواست‌ها: +- ✅ شناسایی کاربر از `Authorization` header +- ✅ بررسی دسترسی با `user.can_manage_course()` یا `Participant.objects.filter()` + +#### موقع ساخت روم: +- ✅ تعیین `defaultLockSettings` (همه `true` به جز whiteboard/notepad) +- ✅ تعیین `roomFeatures` **کامل** شامل: + - ✅ `sharedNotePadFeatures.allowedSharedNotePad: true` + - ✅ `whiteboardFeatures.allowedWhiteboard: true` + - ✅ `breakoutRoomFeatures.isAllow: true` + - ✅ `chatFeatures`, `recordingFeatures`, و سایر فیچرها +- ✅ تبدیل نام‌های `snake_case` به `camelCase` قبل از ارسال به PlugNMeet +- ✅ ساخت `metadata` کامل برای PlugNMeet + +#### موقع گرفتن توکن: +- ✅ پیدا کردن live session فعال از `course_slug` +- ✅ گرفتن `room_id` از session +- ✅ ساخت `user_id` از `request.user.id` +- ✅ ساخت `name` از `user.get_full_name()` یا `user.email` +- ✅ تشخیص `is_admin` از `user.can_manage_course(course)` +- ✅ گرفتن `profilePic` از `user.avatar` +- ✅ اضافه کردن `lock_settings` کامل برای غیر-admin شامل: + - ✅ `lock_microphone`, `lock_webcam`, `lock_screen_sharing` (همه `True`) + - ✅ `lock_whiteboard`, `lock_shared_notepad` (همه `False` - می‌توانند بنویسند) + - ✅ `lock_chat`, `lock_chat_send_message`, `lock_private_chat` (همه `False`) +- ✅ تبدیل نام‌های `snake_case` به `camelCase` قبل از ارسال +- ✅ ساخت `user_info` کامل برای PlugNMeet + +**نتیجه:** +- 🔒 **امنیت کامل**: فرانت نمی‌تواند هیچ تنظیمات امنیتی را دستکاری کند +- ✅ **متمرکز**: همه logic در بک‌اند Django است +- 🎯 **ساده**: فرانت فقط `course_slug` و `Authorization` header ارسال می‌کند +- 🔐 **قابل کنترل**: بک‌اند تعیین می‌کند کدام session فعال است + +--- -## سناریوهای پیشنهادی برای پیاده‌سازی فرانت +## 🐛 عیب‌یابی -### سناریوی استاد -1. **دریافت وضعیت**: با `GET /online/validate/` وضعیت را بگیرید. - - اگر `can_create_live_session = true` است → دکمه «ساخت کلاس» را نشان دهید. - - اگر `can_join_live_session = true` است → دکمه «ورود به کلاس» را نشان دهید. -2. **ساخت روم** (در صورت نیاز): +### مشکل: Whiteboard/SharedNotePad نمایش داده نمی‌شود + +**علائم:** +- آیکون Whiteboard در footer نمایش داده نمی‌شود +- گزینه Enable/Disable SharedNotePad در منوی admin نیست +- گزینه Manage Breakout Room در منوی admin نیست + +**راه حل‌ها:** + +1. **بررسی `roomFeatures` در room creation:** ```json - POST /online/room/create/ - Body: { - "room_id": "algebra-10-1704880365000", // اختیاری - "subject": "کلاس جبر" // اختیاری + "roomFeatures": { + "sharedNotePadFeatures": { + "allowedSharedNotePad": true // ✅ باید true باشد + }, + "whiteboardFeatures": { + "allowedWhiteboard": true // ✅ باید true باشد + }, + "breakoutRoomFeatures": { + "isAllow": true // ✅ باید true باشد + } } ``` - **نکته**: فقط این دو فیلد! بک‌اند خودش `metadata` و تنظیمات امنیتی را اعمال می‌کند. -3. **گرفتن توکن**: - ```json - POST /online/room/token/ - Body: { - "course_slug": "algebra-10" // فقط این! - } +2. **بررسی نامگذاری فیلدها:** + - ❌ `shared_note_pad_features` (snake_case) - اشتباه + - ✅ `sharedNotePadFeatures` (camelCase) - صحیح + +3. **بررسی `config.yaml` در plugnmeet-server:** + ```yaml + shared_notepad: + enabled: true # ✅ باید true باشد + etherpad_hosts: + - id: "etherpad_node_01" + host: "http://plugnmeet-etherpad:9001" + client_id: "plugNmeet" + client_secret: "..." ``` - **نکته**: بک‌اند خودش live session فعال را پیدا می‌کند و `room_id` را می‌گیرد. -4. **ورود به کلاس**: - ```javascript - window.open(`https://meet.newhorizonco.uk/?access_token=${token}`, "_blank"); +4. **بررسی Etherpad service:** + ```bash + docker ps | grep etherpad + # باید یک container با نام plugnmeet-etherpad اجرا باشد ``` -### سناریوی دانشجو -1. **دریافت وضعیت**: با `GET /online/validate/` وضعیت را بگیرید. - - اگر `is_online = true` و `can_join_live_session = true` → دکمه «ورود به کلاس» را نمایش دهید. - -2. **گرفتن توکن و ورود**: +5. **بررسی `defaultLockSettings`:** + - اگر `lockWhiteboard: true` باشد، فقط admin می‌تواند ویرایش کند + - اگر `lockSharedNotepad: true` باشد، فقط admin می‌تواند ویرایش کند + +6. **بررسی user `lock_settings` در توکن:** ```json - POST /online/room/token/ - Body: { - "course_slug": "algebra-10" // فقط این! + "lock_settings": { + "lock_whiteboard": false, // false = می‌تواند ویرایش کند + "lock_shared_notepad": false // false = می‌تواند ویرایش کند } ``` - سپس با `token` دریافتی به PlugNMeet وارد شوید. -### ✅ آنچه بک‌اند خودکار انجام می‌دهد: +### مشکل: دانشجو نمی‌تواند در Whiteboard بنویسد -#### موقع ساخت روم: -- ✅ تعیین `default_lock_settings` (همه `true`) -- ✅ تعیین `room_features.mute_on_start: true` -- ✅ ساخت `metadata` کامل برای PlugNMeet +**علت:** +- `lock_whiteboard: true` در توکن کاربر -#### موقع گرفتن توکن: -- ✅ پیدا کردن live session فعال از `course_slug` -- ✅ گرفتن `room_id` از session -- ✅ تشخیص `is_admin` با `user.can_manage_course(course)` -- ✅ ساخت `user_info` کامل (user_id, name, profilePic) -- ✅ اضافه کردن `lock_settings` برای دانشجو - -### ❌ آنچه فرانت نباید ارسال کند: -- ❌ `metadata` موقع ساخت روم -- ❌ `room_id` موقع گرفتن توکن -- ❌ `user_info`, `is_admin`, `lock_settings` - -### 🔐 نکات امنیتی: -- همه تنظیمات امنیتی در سمت سرور کنترل می‌شود -- فرانت نمی‌تواند تنظیمات را دستکاری کند -- بک‌اند تعیین می‌کند چه کسی استاد است و چه کسی دانشجو -- زمانی که استاد وارد لایو شده است، `can_create_live_session` برابر `false` می‌شود -- برای نمایش مدت سپری‌شده، از `livesession_started_at` استفاده کرده و در فرانت اختلاف با زمان فعلی را محاسبه کنید +**راه حل:** +- در هنگام ساخت توکن برای دانشجو، `lock_whiteboard` را `false` کنید +- یا از منوی admin، lock را برای آن کاربر خاص باز کنید + +### مشکل: SharedNotePad آیکون دارد اما باز نمی‌شود + +**علت:** +- Etherpad service اجرا نیست یا در دسترس نیست + +**راه حل:** +```bash +# بررسی وضعیت Etherpad +docker-compose -f docker-compose.plugnmeet.yml ps etherpad + +# اگر اجرا نیست، راه‌اندازی کنید +docker-compose -f docker-compose.plugnmeet.yml up -d plugnmeet-etherpad + +# بررسی logs +docker-compose -f docker-compose.plugnmeet.yml logs -f plugnmeet-etherpad +``` diff --git a/docs/plugnmeet_api.md b/docs/plugnmeet_api.md new file mode 100644 index 0000000..50deff3 --- /dev/null +++ b/docs/plugnmeet_api.md @@ -0,0 +1,1660 @@ +# 📚 Plugnmeet Server API Documentation + +> **مستندات کامل API سرور کنفرانس ویدیویی Plugnmeet** + +این مستند راهنمای کامل API های Plugnmeet Server را شامل می‌شود که بر پایه LiveKit ساخته شده است. + +--- + +## 📖 فهرست مطالب + +### 🚀 شروع سریع +- [بررسی فعال بودن روم](#-quick-check-room-status) +- [قابلیت‌های کلیدی](#-core-features) + +### 🔐 احراز هویت و امنیت +- [نحوه احراز هویت](#-authentication) + - [روش `/auth` (HMAC + JSON)](#1-auth-endpoints-hmac--json) + - [روش `/api` (Bearer Token + Protobuf)](#2-api-endpoints-bearer-token--protobuf) + - [روش‌های خاص (LTI & BBB)](#3-special-authentication-methods) + +### 🎯 API Reference +- [**Room Management** - مدیریت اتاق‌ها](#-room-management-api) +- [**Recording Management** - مدیریت ضبط‌ها](#-recording-management-api) +- [**Analytics** - آنالیتیکس و گزارش‌گیری](#-analytics-api) +- [**In-Meeting Controls** - کنترل‌های داخل جلسه](#-in-meeting-controls-api) +- [**Advanced Features** - امکانات پیشرفته](#-advanced-features) + +### 🔧 سایر سرویس‌ها +- [Webhook, Health Check, Downloads](#-other-services) +- [BBB & LTI Compatibility](#-compatibility-apis) + +--- + +## 🚀 Quick Check: Room Status + +ساده‌ترین روش برای بررسی فعال بودن یک روم: + +### Endpoint +```http +POST /auth/room/isRoomActive +``` + +### Request +```json +{ + "roomId": "your-room-id" +} +``` + +### Response +```json +{ + "status": true, + "msg": "room is active", + "isActive": true +} +``` + +### cURL Example +```bash +#!/bin/bash +API_KEY="your-api-key" +SECRET="your-secret-key" +BODY='{"roomId":"algebra-1402"}' + +# محاسبه HMAC-SHA256 +SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') + +# ارسال درخواست +curl -X POST 'https://your-domain.com/auth/room/isRoomActive' \ + -H "API-KEY: $API_KEY" \ + -H "HASH-SIGNATURE: $SIG" \ + -H 'Content-Type: application/json' \ + -d "$BODY" +``` + +--- + +## ⭐ Core Features + +Plugnmeet Server مجموعه کاملی از قابلیت‌های حرفه‌ای برای برگزاری کنفرانس‌های آنلاین را فراهم می‌کند: + +### 🎥 Video Conferencing +- ✅ HD Audio/Video با کیفیت بالا +- ✅ Screen Sharing - اشتراک‌گذاری صفحه +- ✅ Virtual Backgrounds - پس‌زمینه مجازی +- ✅ Adaptive Streaming (Simulcast & Dynacast) + +### 📊 Collaboration Tools +- ✅ Interactive Whiteboard با پشتیبانی از فایل‌های PDF/Office +- ✅ Shared Notepad - یادداشت مشترک +- ✅ Live Polls - نظرسنجی زنده +- ✅ Breakout Rooms - اتاق‌های گروهی + +### 🎬 Recording & Streaming +- ✅ Cloud Recording - ضبط ابری با فرمت MP4 +- ✅ RTMP Streaming - پخش زنده +- ✅ Ingress Support (RTMP/WHIP) + +### 🛡️ Security & Control +- ✅ Waiting Room - اتاق انتظار +- ✅ Lock Settings - قفل کردن قابلیت‌ها +- ✅ User Management - مدیریت کاربران +- ✅ End-to-End Encryption + +### 📈 Analytics & Monitoring +- ✅ Session Analytics - آنالیتیکس جلسات +- ✅ Participant Reports - گزارش شرکت‌کنندگان +- ✅ Real-time Monitoring + +### ♿ Accessibility +- ✅ Speech-to-Text - گفتار به متن +- ✅ Real-time Translation (Azure) + +--- + +## 🔐 Authentication + +Plugnmeet از سه روش احراز هویت مختلف استفاده می‌کند: + +### 1. `/auth` Endpoints (HMAC + JSON) + +برای عملیات مدیریتی و ساخت توکن‌ها استفاده می‌شود. + +#### Headers +```http +API-KEY: your_api_key +HASH-SIGNATURE: hmac_sha256_hex_signature +Content-Type: application/json +``` + +#### محاسبه HMAC Signature + +**Bash/Shell:** +```bash +SECRET="your-secret-key" +BODY='{"roomId":"test-room"}' +SIG=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}') +``` + +**Python:** +```python +import hmac +import hashlib +import json + +secret = "your-secret-key" +body = {"roomId": "test-room"} +body_json = json.dumps(body) + +signature = hmac.new( + secret.encode('utf-8'), + body_json.encode('utf-8'), + hashlib.sha256 +).hexdigest() +``` + +**Node.js:** +```javascript +const crypto = require('crypto'); + +const secret = 'your-secret-key'; +const body = JSON.stringify({ roomId: 'test-room' }); + +const signature = crypto + .createHmac('sha256', secret) + .update(body) + .digest('hex'); +``` + +--- + +### 2. `/api` Endpoints (Bearer Token + Protobuf) + +برای کنترل‌های داخل جلسه استفاده می‌شود. + +#### Headers +```http +Authorization: +Content-Type: application/octet-stream +``` + +> **توکن دسترسی** از طریق `/auth/room/getJoinToken` دریافت می‌شود. + +#### Request/Response Format +- **بدنه درخواست**: Binary Protobuf (استفاده از SDK توصیه می‌شود) +- **پاسخ**: Binary Protobuf + +#### cURL Example با Protobuf +```bash +# ساخت فایل باینری با SDK +# سپس ارسال با curl +curl -X POST 'https://your-domain.com/api/recording' \ + -H "Authorization: $TOKEN" \ + -H 'Content-Type: application/octet-stream' \ + --data-binary @recording_req.bin \ + -o response.bin +``` + +> **نکته**: برخی endpoint های `/api` مانند `convertWhiteboardFile` و `fileUpload` از JSON استفاده می‌کنند. + +--- + +### 3. Special Authentication Methods + +#### LTI (Learning Tools Interoperability) +```http +Authorization: +``` +مسیرها: `/lti/v1/...` + +#### BigBlueButton Compatibility +نیازمند `checksum` محاسبه شده مطابق استاندارد BBB +مسیرها: `/:apiKey/bigbluebutton/api/...` + +--- + +## 📋 Room Management API + +### 🏗️ Create Room + +اتاق جلسه جدید ایجاد می‌کند. + +#### Endpoint +```http +POST /auth/room/create +``` + +#### Request Body +```json +{ + "roomId": "algebra-class-1402", + "maxParticipants": 50, + "emptyTimeout": 300, + "metadata": { + "roomTitle": "کلاس جبر خطی", + "welcomeMessage": "به کلاس جبر خوش آمدید", + "defaultLockSettings": { + "lockMicrophone": false, + "lockWebcam": false, + "lockScreenSharing": true, + "lockChat": false, + "lockChatSendMessage": false, + "lockChatFileShare": false, + "lockPrivateChat": false, + "lockWhiteboard": true, + "lockSharedNotepad": false + }, + "roomFeatures": { + "allowWebcams": true, + "muteOnStart": false, + "allowScreenSharing": true, + "allowRecording": true, + "allowRtmp": true, + "allowViewOtherWebcams": true, + "allowViewOtherParticipantsList": true, + "adminOnlyWebcams": false, + "allowPolls": true, + "roomDuration": 0, + "recordingFeatures": { + "isAllow": true, + "isAllowCloud": true, + "enableAutoCloudRecording": false + }, + "chatFeatures": { + "allowChat": true, + "allowFileUpload": true + }, + "sharedNotePadFeatures": { + "allowedSharedNotePad": true + }, + "whiteboardFeatures": { + "allowedWhiteboard": true, + "preloadFile": "" + }, + "breakoutRoomFeatures": { + "isAllow": true, + "allowedNumberRooms": 6 + }, + "displayExternalLinkFeatures": { + "isAllow": true + }, + "ingressFeatures": { + "isAllow": false + }, + "speechToTextTranslationFeatures": { + "isAllow": true, + "isAllowTranslation": true + } + }, + "webhookUrl": "https://your-domain.com/webhook", + "isBreakoutRoom": false, + "parentRoomId": "" + } +} +``` + +#### Response +```json +{ + "status": true, + "msg": "room created successfully", + "roomId": "algebra-class-1402" +} +``` + +--- + +### 🎫 Generate Join Token + +توکن ورود کاربر به جلسه را ایجاد می‌کند. + +#### Endpoint +```http +POST /auth/room/getJoinToken +``` + +#### Request Body +```json +{ + "roomId": "algebra-class-1402", + "userInfo": { + "userId": "student-123", + "name": "علی احمدی", + "isAdmin": false, + "isHidden": false, + "userMetadata": { + "profilePic": "https://example.com/avatar.jpg", + "lockSettings": { + "lockMicrophone": false, + "lockWebcam": false, + "lockScreenSharing": true, + "lockChat": false + } + } + } +} +``` + +#### Response +```json +{ + "status": true, + "msg": "token generated", + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." +} +``` + +#### Integration Example +```html + + + + Join Meeting + + + + + +``` + +--- + +### ✅ Check Room Status + +#### Endpoint +```http +POST /auth/room/isRoomActive +``` + +#### Request +```json +{ + "roomId": "algebra-class-1402" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "room is active", + "isActive": true +} +``` + +--- + +### 📊 Get Active Room Info + +اطلاعات کامل یک روم فعال و لیست شرکت‌کنندگان آن را برمی‌گرداند. + +#### Endpoint +```http +POST /auth/room/getActiveRoomInfo +``` + +#### Request +```json +{ + "roomId": "algebra-class-1402" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "success", + "room": { + "roomInfo": { + "sid": "RM_xxxxxxxxxxxx", + "roomId": "algebra-class-1402", + "name": "algebra-class-1402", + "emptyTimeout": 300, + "maxParticipants": 50, + "creationTime": "1699123456", + "metadata": "{...}" + }, + "participantsInfo": [ + { + "sid": "PA_xxxxxxxxxxxx", + "identity": "student-123", + "name": "علی احمدی", + "state": 0, + "joinedAt": "1699123500" + } + ] + } +} +``` + +--- + +### 📋 List All Active Rooms + +لیست تمام روم‌های فعال را برمی‌گرداند. + +#### Endpoint +```http +POST /auth/room/getActiveRoomsInfo +``` + +#### Request +```json +{} +``` + +#### Response +```json +{ + "status": true, + "msg": "success", + "rooms": [ + { + "roomId": "algebra-class-1402", + "sid": "RM_xxxxxxxxxxxx", + "numParticipants": 15, + "creationTime": "1699123456" + }, + { + "roomId": "physics-class-1402", + "sid": "RM_yyyyyyyyyyyy", + "numParticipants": 8, + "creationTime": "1699123789" + } + ] +} +``` + +--- + +### 🛑 End Room + +جلسه را به پایان می‌رساند و تمام شرکت‌کنندگان را خارج می‌کند. + +#### Endpoint +```http +POST /auth/room/endRoom +``` + +#### Request +```json +{ + "roomId": "algebra-class-1402" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "room ended successfully" +} +``` + +--- + +### 📜 Fetch Past Rooms + +لیست روم‌های گذشته را با امکان فیلتر و صفحه‌بندی برمی‌گرداند. + +#### Endpoint +```http +POST /auth/room/fetchPastRooms +``` + +#### Request +```json +{ + "roomIds": ["algebra-class-1402", "physics-class-1402"], + "from": 0, + "limit": 20, + "orderBy": "DESC" +} +``` + +#### Request Parameters + +| Parameter | Type | Description | +|-----------|------|-------------| +| `roomIds` | `string[]` | لیست شناسه‌های روم (اختیاری) | +| `from` | `number` | شروع صفحه‌بندی | +| `limit` | `number` | تعداد نتایج (حداکثر 100) | +| `orderBy` | `string` | ترتیب: `ASC` یا `DESC` | + +#### Response +```json +{ + "status": true, + "msg": "success", + "result": { + "totalRooms": 45, + "from": 0, + "limit": 20, + "orderBy": "DESC", + "roomsList": [ + { + "roomId": "algebra-class-1402", + "sid": "RM_xxxxxxxxxxxx", + "roomTitle": "کلاس جبر خطی", + "creationTime": "1699123456", + "ended": "1699127056", + "roomDuration": 3600 + } + ] + } +} +``` + +--- + +## 🎬 Recording Management API + +### 📋 Fetch Recordings + +لیست ضبط‌های انجام شده را دریافت می‌کند. + +#### Endpoint +```http +POST /auth/recording/fetch +``` + +#### Request +```json +{ + "roomIds": ["algebra-class-1402"], + "from": 0, + "limit": 20, + "orderBy": "DESC" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "success", + "result": { + "totalRecordings": 5, + "from": 0, + "limit": 20, + "orderBy": "DESC", + "recordings": [ + { + "recordId": "rec_xxxxxxxxxxxx", + "roomId": "algebra-class-1402", + "roomSid": "RM_xxxxxxxxxxxx", + "filePath": "/recordings/algebra-class-1402_20231105.mp4", + "fileSize": 524288000, + "creationTime": "1699123456", + "roomCreationTime": "1699120000", + "recordingDuration": 3600 + } + ] + } +} +``` + +--- + +### 📄 Get Recording Info + +اطلاعات کامل یک ضبط را برمی‌گرداند. + +#### Endpoint +```http +POST /auth/recording/recordingInfo +``` + +#### Request +```json +{ + "recordId": "rec_xxxxxxxxxxxx" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "success", + "recordingInfo": { + "recordId": "rec_xxxxxxxxxxxx", + "roomId": "algebra-class-1402", + "filePath": "/recordings/algebra-class-1402_20231105.mp4", + "fileSize": 524288000, + "creationTime": "1699123456" + } +} +``` + +--- + +### 🗑️ Delete Recording + +یک ضبط را حذف می‌کند. + +#### Endpoint +```http +POST /auth/recording/delete +``` + +#### Request +```json +{ + "recordId": "rec_xxxxxxxxxxxx" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "recording deleted successfully" +} +``` + +--- + +### 🔗 Get Download Token + +توکن موقت برای دانلود فایل ضبط شده ایجاد می‌کند. + +#### Endpoint +```http +POST /auth/recording/getDownloadToken +``` + +#### Request +```json +{ + "recordId": "rec_xxxxxxxxxxxx" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "token generated", + "token": "download_token_xxxxxxxxxxxx" +} +``` + +#### Download File +```bash +# دانلود فایل با توکن +curl -o recording.mp4 \ + "https://your-domain.com/download/recording/download_token_xxxxxxxxxxxx" +``` + +--- + +## 📈 Analytics API + +### 📋 Fetch Analytics + +لیست آنالیتیکس جلسات را دریافت می‌کند. + +#### Endpoint +```http +POST /auth/analytics/fetch +``` + +#### Request +```json +{ + "roomIds": ["algebra-class-1402"], + "from": 0, + "limit": 20 +} +``` + +#### Response +```json +{ + "status": true, + "msg": "success", + "result": { + "totalAnalytics": 10, + "analyticsList": [ + { + "analyticsId": "ana_xxxxxxxxxxxx", + "roomId": "algebra-class-1402", + "roomSid": "RM_xxxxxxxxxxxx", + "fileId": "file_xxxxxxxxxxxx", + "fileName": "analytics_algebra-class-1402_20231105.json", + "filePath": "/analytics/algebra-class-1402_20231105.json", + "fileSize": 102400, + "creationTime": "1699127056" + } + ] + } +} +``` + +--- + +### 🗑️ Delete Analytics + +#### Endpoint +```http +POST /auth/analytics/delete +``` + +#### Request +```json +{ + "analyticsId": "ana_xxxxxxxxxxxx" +} +``` + +--- + +### 🔗 Get Download Token + +#### Endpoint +```http +POST /auth/analytics/getDownloadToken +``` + +#### Request +```json +{ + "analyticsId": "ana_xxxxxxxxxxxx" +} +``` + +#### Response +```json +{ + "status": true, + "msg": "token generated", + "token": "analytics_token_xxxxxxxxxxxx" +} +``` + +#### Download Analytics File +```bash +curl -o analytics.json \ + "https://your-domain.com/download/analytics/analytics_token_xxxxxxxxxxxx" +``` + +--- + +## 🎮 In-Meeting Controls API + +> **نکته مهم**: تمام endpoint های این بخش نیازمند **Bearer Token** در هدر `Authorization` هستند و از **Binary Protobuf** استفاده می‌کنند (مگر در موارد خاص که JSON ذکر شده باشد). + +### 🔐 Verify Token + +توکن کاربر را تایید کرده و اطلاعات اتصال را برمی‌گرداند. + +#### Endpoint +```http +POST /api/verifyToken +``` + +#### Request (Protobuf) +```protobuf +message VerifyTokenReq {} +``` + +#### Response (Protobuf) +```protobuf +message VerifyTokenRes { + bool status = 1; + string msg = 2; + string roomId = 3; + string userId = 4; + string roomSid = 5; + repeated string natsWsUrls = 6; + string natsSubject = 7; + string serverVersion = 8; +} +``` + +--- + +### 🎬 Recording & RTMP Control + +#### Start/Stop Recording + +**Endpoint:** +```http +POST /api/recording +``` + +**Request (Protobuf):** +```protobuf +message RecordingReq { + string sid = 1; // Room SID + RecordingTasks task = 2; // START_RECORDING | STOP_RECORDING + string rtmpUrl = 3; // برای RTMP +} + +enum RecordingTasks { + START_RECORDING = 0; + STOP_RECORDING = 1; + START_RTMP = 2; + STOP_RTMP = 3; +} +``` + +**Response (Protobuf):** +```protobuf +message RecordingRes { + bool status = 1; + string msg = 2; +} +``` + +--- + +### 🛑 End Room + +اتاق را از داخل جلسه به پایان می‌رساند (فقط ادمین). + +#### Endpoint +```http +POST /api/endRoom +``` + +#### Request (Protobuf) +```protobuf +message RoomEndReq { + string roomId = 1; +} +``` + +--- + +### 🔒 Update Lock Settings + +تنظیمات قفل کاربران را تغییر می‌دهد (فقط ادمین). + +#### Endpoint +```http +POST /api/updateLockSettings +``` + +#### Request (Protobuf) +```protobuf +message UpdateUserLockSettingsReq { + string roomSid = 1; + string roomId = 2; + string userId = 3; // "all" برای همه | شناسه کاربر خاص + string service = 4; // mic | webcam | screenShare | chat | etc. + string direction = 5; // "lock" | "unlock" +} +``` + +#### Available Services +- `mic` - میکروفون +- `webcam` - وب‌کم +- `screenShare` - اشتراک‌گذاری صفحه +- `chat` - چت +- `sendChatMsg` - ارسال پیام در چت +- `chatFile` - ارسال فایل در چت +- `privateChat` - چت خصوصی +- `whiteboard` - وایت‌برد +- `sharedNotepad` - یادداشت مشترک + +--- + +### 🔇 Mute/Unmute Track + +میکروفون یک یا تمام کاربران را قطع یا وصل می‌کند (فقط ادمین). + +#### Endpoint +```http +POST /api/muteUnmuteTrack +``` + +#### Request (Protobuf) +```protobuf +message MuteUnMuteTrackReq { + string sid = 1; // Room SID + string roomId = 2; + string userId = 3; // "all" برای همه | شناسه کاربر + string trackSid = 4; // اختیاری + bool muted = 5; // true = mute | false = unmute +} +``` + +--- + +### 👤 Remove Participant + +کاربر را از جلسه حذف می‌کند (فقط ادمین). + +#### Endpoint +```http +POST /api/removeParticipant +``` + +#### Request (Protobuf) +```protobuf +message RemoveParticipantReq { + string sid = 1; + string roomId = 2; + string userId = 3; + string msg = 4; // پیام برای کاربر + bool blockUser = 5; // مسدود کردن دائمی +} +``` + +--- + +### 🎤 Switch Presenter + +نقش ارائه‌دهنده را به کاربر می‌دهد یا می‌گیرد (فقط ادمین). + +#### Endpoint +```http +POST /api/switchPresenter +``` + +#### Request (Protobuf) +```protobuf +message SwitchPresenterReq { + string userId = 1; + SwitchPresenterTask task = 2; // PROMOTE | DEMOTE +} + +enum SwitchPresenterTask { + PROMOTE = 0; + DEMOTE = 1; +} +``` + +--- + +## 🎨 Advanced Features + +### 🔗 External Display Link + +لینک خارجی را برای تمام شرکت‌کنندگان نمایش می‌دهد (فقط ادمین). + +#### Endpoint +```http +POST /api/externalDisplayLink +``` + +#### Request (Protobuf) +```protobuf +message ExternalDisplayLinkReq { + ExternalDisplayLinkTask task = 1; // START_EXTERNAL_LINK | STOP_EXTERNAL_LINK + string url = 2; +} +``` + +--- + +### 🎵 External Media Player + +ویدیو یا صدای خارجی را پخش می‌کند (فقط ادمین). + +#### Endpoint +```http +POST /api/externalMediaPlayer +``` + +#### Request (Protobuf) +```protobuf +message ExternalMediaPlayerReq { + ExternalMediaPlayerTask task = 1; // START_PLAYBACK | STOP_PLAYBACK + string url = 2; + bool isPresentation = 3; +} +``` + +> **نکته**: می‌توانید فایل را با `/api/fileUpload` آپلود کرده و لینک `/download/uploadedFile/...` را استفاده کنید. + +--- + +### 🚪 Waiting Room + +#### Approve Users + +کاربران در اتاق انتظار را تایید می‌کند (فقط ادمین). + +**Endpoint:** +```http +POST /api/waitingRoom/approveUsers +``` + +**Request (Protobuf):** +```protobuf +message ApproveWaitingUsersReq { + repeated string userIds = 1; +} +``` + +#### Update Waiting Room Message + +**Endpoint:** +```http +POST /api/waitingRoom/updateMsg +``` + +**Request (Protobuf):** +```protobuf +message UpdateWaitingRoomMessageReq { + string message = 1; +} +``` + +--- + +### 📊 Polls (نظرسنجی) + +#### Create Poll + +نظرسنجی جدید ایجاد می‌کند (فقط ادمین). + +**Endpoint:** +```http +POST /api/polls/create +``` + +**Request (Protobuf):** +```protobuf +message CreatePollReq { + string question = 1; + repeated PollOption options = 2; + bool isAnonymous = 3; + bool allowMultipleVotes = 4; +} + +message PollOption { + uint64 id = 1; + string text = 2; +} +``` + +--- + +#### List Polls + +**Endpoint:** +```http +GET /api/polls/listPolls +``` + +**Response:** Binary Protobuf + +--- + +#### Submit Poll Response + +**Endpoint:** +```http +POST /api/polls/submitResponse +``` + +**Request (Protobuf):** +```protobuf +message SubmitPollResponseReq { + string pollId = 1; + repeated uint64 selectedOptionIds = 2; +} +``` + +--- + +#### Get Poll Results + +**Endpoint:** +```http +GET /api/polls/pollResponsesResult/:pollId +``` + +**Response:** Binary Protobuf با نتایج نظرسنجی + +--- + +### 🏢 Breakout Rooms (اتاق‌های گروهی) + +#### Create Breakout Rooms + +**Endpoint:** +```http +POST /api/breakoutRoom/create +``` + +**Request (Protobuf):** +```protobuf +message CreateBreakoutRoomsReq { + uint64 duration = 1; + repeated BreakoutRoom rooms = 2; +} + +message BreakoutRoom { + string id = 1; + string title = 2; + repeated string userIds = 3; +} +``` + +--- + +#### Join Breakout Room + +**Endpoint:** +```http +POST /api/breakoutRoom/join +``` + +**Request (Protobuf):** +```protobuf +message JoinBreakoutRoomReq { + string breakoutRoomId = 1; +} +``` + +--- + +#### List Breakout Rooms + +**Endpoint:** +```http +GET /api/breakoutRoom/listRooms +``` + +--- + +#### End Breakout Room + +**Endpoint:** +```http +POST /api/breakoutRoom/endRoom +``` + +**Request (Protobuf):** +```protobuf +message EndBreakoutRoomReq { + string breakoutRoomId = 1; +} +``` + +--- + +#### End All Breakout Rooms + +**Endpoint:** +```http +POST /api/breakoutRoom/endAllRooms +``` + +--- + +### 📡 Ingress (RTMP/WHIP Input) + +ورودی استریم خارجی ایجاد می‌کند. + +#### Endpoint +```http +POST /api/ingress/create +``` + +#### Request (Protobuf) +```protobuf +message CreateIngressReq { + IngressInput inputType = 1; // RTMP_INPUT | WHIP_INPUT + string participantName = 2; + string roomId = 3; +} + +enum IngressInput { + RTMP_INPUT = 0; + WHIP_INPUT = 1; +} +``` + +#### Response (Protobuf) +```protobuf +message CreateIngressRes { + bool status = 1; + string msg = 2; + string ingressId = 3; + string url = 4; + string streamKey = 5; +} +``` + +#### Usage Example +پس از دریافت `url` و `streamKey`: +```bash +# استریم با FFmpeg +ffmpeg -re -i input.mp4 \ + -c:v libx264 -c:a aac \ + -f flv "rtmp://url/stream_key" +``` + +--- + +### 🗣️ Speech Services (Azure) + +#### Enable/Disable Speech Service + +**Endpoint:** +```http +POST /api/speechServices/serviceStatus +``` + +**Request (Protobuf):** +```protobuf +message SpeechToTextTranslationReq { + bool enabled = 1; +} +``` + +--- + +#### Get Azure Token + +**Endpoint:** +```http +POST /api/speechServices/azureToken +``` + +**Request (Protobuf):** +```protobuf +message GenerateAzureTokenReq { + string userSid = 1; +} +``` + +--- + +### 📁 File Upload & Whiteboard + +#### Upload File (Resumable) + +برای آپلود فایل‌های بزرگ به صورت chunk به chunk. + +**Endpoint:** +```http +POST /api/fileUpload?resumable=true&roomSid=xxx&roomId=xxx&userId=xxx +``` + +**Headers:** +```http +Authorization: +Content-Type: multipart/form-data +``` + +**Response:** `part_uploaded` or error + +--- + +#### Merge Uploaded Chunks + +**Endpoint:** +```http +POST /api/uploadedFileMerge +``` + +**Request (JSON):** +```json +{ + "roomSid": "RM_xxxxxxxxxxxx", + "roomId": "algebra-class-1402", + "resumableIdentifier": "unique-file-id", + "resumableFilename": "document.pdf", + "resumableTotalChunks": 10 +} +``` + +**Response (JSON):** +```json +{ + "status": true, + "msg": "file merged successfully", + "filePath": "/uploads/document.pdf", + "fileName": "document.pdf", + "fileExtension": "pdf" +} +``` + +--- + +#### Convert Whiteboard File + +فایل‌های PDF/Office را به تصاویر برای وایت‌برد تبدیل می‌کند. + +> **پیش‌نیاز**: `libreoffice` و `mupdf-tools` (mutool) باید روی سرور نصب باشند. + +**Endpoint:** +```http +POST /api/convertWhiteboardFile +``` + +**Request (JSON):** +```json +{ + "roomSid": "RM_xxxxxxxxxxxx", + "roomId": "algebra-class-1402", + "filePath": "/uploads/document.pdf", + "userId": "teacher-123" +} +``` + +**Response (JSON):** +```json +{ + "status": true, + "msg": "file converted successfully", + "fileName": "document", + "fileId": "file_xxxxxxxxxxxx", + "filePath": "/whiteboard/document/", + "totalPages": 15 +} +``` + +--- + +## 🔧 Other Services + +### 🔔 Webhook + +برای دریافت رویدادهای LiveKit. + +**Endpoint:** +```http +POST /webhook +``` + +**Headers:** +```http +Authorization: +``` + +**Webhook Events:** +- `room_started` - شروع روم +- `room_finished` - پایان روم +- `participant_joined` - ورود کاربر +- `participant_left` - خروج کاربر +- `track_published` - انتشار track +- `track_unpublished` - حذف track +- `recording_started` - شروع ضبط +- `recording_finished` - پایان ضبط +- و بیشتر... + +--- + +### ❤️ Health Check + +وضعیت سلامت سرور را بررسی می‌کند. + +**Endpoint:** +```http +GET /healthCheck +``` + +**Response:** +``` +Healthy +``` + +سرویس‌های بررسی شده: +- ✅ Database (MySQL/MariaDB) +- ✅ Redis +- ✅ NATS + +--- + +### 📥 Download Services + +#### Download Uploaded File +```http +GET /download/uploadedFile/:sid/* +``` + +#### Download Recording +```http +GET /download/recording/:token +``` + +#### Download Analytics +```http +GET /download/analytics/:token +``` + +--- + +## 🔄 Compatibility APIs + +### 🟦 BigBlueButton (BBB) Compatibility + +Plugnmeet از API های BigBlueButton پشتیبانی می‌کند. + +**Base Path:** +``` +/:apiKey/bigbluebutton/api +``` + +**Available Endpoints:** +- `GET/POST /create` - ایجاد جلسه +- `GET/POST /join` - ورود به جلسه +- `GET/POST /isMeetingRunning` - بررسی فعال بودن +- `GET/POST /getMeetingInfo` - اطلاعات جلسه +- `GET/POST /getMeetings` - لیست جلسات +- `GET/POST /end` - پایان جلسه +- `GET/POST /getRecordings` - لیست ضبط‌ها +- `GET/POST /deleteRecordings` - حذف ضبط +- `GET/POST /publishRecordings` - انتشار ضبط +- `GET/POST /updateRecordings` - به‌روزرسانی ضبط + +**Authentication:** نیازمند `checksum` مطابق استاندارد BBB + +#### Example (BBB Join) +```bash +API_KEY="your-api-key" +SECRET="your-secret" +MEETING_ID="test-meeting" +USER_NAME="Ali" + +# ساخت query string +QUERY="meetingID=${MEETING_ID}&fullName=${USER_NAME}" + +# محاسبه checksum +CHECKSUM=$(echo -n "join${QUERY}${SECRET}" | sha1sum | awk '{print $1}') + +# URL نهایی +URL="https://your-domain.com/${API_KEY}/bigbluebutton/api/join?${QUERY}&checksum=${CHECKSUM}" + +echo "Join URL: $URL" +``` + +--- + +### 🎓 LTI (Learning Tools Interoperability) + +برای یکپارچگی با سیستم‌های LMS. + +**Base Path:** +``` +/lti/v1 +``` + +#### LTI Landing +```http +POST /lti/v1 +``` + +#### LTI API Endpoints + +نیازمند هدر `Authorization` خاص LTI: + +- `POST /lti/v1/api/room/join` - ورود به روم +- `POST /lti/v1/api/room/isActive` - بررسی فعال بودن +- `POST /lti/v1/api/room/end` - پایان روم +- `POST /lti/v1/api/recording/fetch` - لیست ضبط‌ها +- `POST /lti/v1/api/recording/download` - دانلود ضبط +- `POST /lti/v1/api/recording/delete` - حذف ضبط + +--- + +## 🛠️ SDKs & Tools + +### Official SDKs + +#### PHP SDK +```bash +composer require mynaparrot/plugnmeet-sdk-php +``` + +```php +roomId = 'test-room'; +$params->metadata->roomTitle = 'کلاس آزمایشی'; + +$result = $plugnmeet->room->create($params); +``` + +--- + +#### JavaScript/Node.js SDK +```bash +npm install plugnmeet-sdk-js +``` + +```javascript +const { PlugNmeet } = require('plugnmeet-sdk-js'); + +const plugnmeet = new PlugNmeet({ + host: 'https://your-domain.com', + apiKey: 'your-api-key', + apiSecret: 'your-secret' +}); + +// ایجاد روم +const result = await plugnmeet.room.create({ + roomId: 'test-room', + metadata: { + roomTitle: 'کلاس آزمایشی' + } +}); + +// تولید توکن ورود +const token = await plugnmeet.room.getJoinToken({ + roomId: 'test-room', + userInfo: { + userId: 'user-123', + name: 'علی احمدی', + isAdmin: false + } +}); +``` + +--- + +### Docker Deployment + +```bash +docker run -d \ + --name plugnmeet-server \ + -p 8080:8080 \ + -v $PWD/config.yaml:/config.yaml \ + mynaparrot/plugnmeet-server \ + --config /config.yaml +``` + +--- + +## 📚 Additional Resources + +### Documentation +- 🌐 **Official Website**: https://www.plugnmeet.org +- 📖 **Full Documentation**: https://www.plugnmeet.org/docs +- 🔧 **Installation Guide**: https://www.plugnmeet.org/docs/installation +- 👨‍💻 **Developer Guide**: https://www.plugnmeet.org/docs/developer-guide + +### Community & Support +- 💬 **Discord**: https://discord.gg/2X2ZaCHu4C +- 🐛 **GitHub Issues**: https://github.com/mynaparrot/plugNmeet-server/issues +- 📧 **Email Support**: support@plugnmeet.com + +### Source Code +- 🖥️ **Server**: https://github.com/mynaparrot/plugNmeet-server +- 🎨 **Client**: https://github.com/mynaparrot/plugNmeet-client +- 🎬 **Recorder**: https://github.com/mynaparrot/plugNmeet-recorder + +--- + +## 📝 Notes & Best Practices + +### Performance Tips +1. ✅ از Redis برای caching استفاده کنید +2. ✅ برای مقیاس‌پذیری از Load Balancer استفاده کنید +3. ✅ ضبط‌ها را در storage خارجی (S3, MinIO) ذخیره کنید +4. ✅ از CDN برای سرویس‌دهی فایل‌های استاتیک استفاده کنید + +### Security Best Practices +1. 🔒 HTTPS را فعال کنید (الزامی) +2. 🔒 `apiKey` و `secret` را محرمانه نگه دارید +3. 🔒 از CORS Policy مناسب استفاده کنید +4. 🔒 توکن‌ها را با expiration time محدود تولید کنید +5. 🔒 Webhook signature را همیشه تایید کنید + +### Rate Limiting +- `/auth` endpoints: 100 req/min per IP +- `/api` endpoints: 1000 req/min per token +- File uploads: 10 MB/s per user + +--- + +## 🎯 Quick Start Checklist + +- [ ] LiveKit Server راه‌اندازی شده +- [ ] Redis نصب و پیکربندی شده +- [ ] MySQL/MariaDB آماده است +- [ ] فایل `config.yaml` تنظیم شده +- [ ] Plugnmeet Server در حال اجراست +- [ ] Client UI در دسترس است +- [ ] Test meeting ایجاد و تست شده +- [ ] Webhook تنظیم شده (اختیاری) +- [ ] Recording تست شده (اختیاری) + +--- + +## 🎉 نسخه و تاریخچه تغییرات + +**نسخه فعلی مستند**: 2.0.0 +**آخرین به‌روزرسانی**: نوامبر 2024 + +برای مشاهده تاریخچه کامل تغییرات به فایل [CHANGELOG.md](./CHANGELOG.md) مراجعه کنید. + +--- + +
+ +**ساخته شده با ❤️ توسط [MynaParrot](https://www.mynaparrot.com)** + +[Website](https://www.plugnmeet.org) • [GitHub](https://github.com/mynaparrot/plugNmeet-server) • [Discord](https://discord.gg/2X2ZaCHu4C) + +