From 10a3a1c51cf36265562a4223d05c091a5ae024a2 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Thu, 5 Feb 2026 13:03:32 +0330 Subject: [PATCH] Add Plugnmeet Server API Documentation and Enhance Live Session Verification --- apps/course/services/api.md | 1660 +++++++++++++++++++++++++++++ apps/course/views/course.py | 87 +- apps/course/views/live_session.py | 65 +- 3 files changed, 1808 insertions(+), 4 deletions(-) create mode 100644 apps/course/services/api.md diff --git a/apps/course/services/api.md b/apps/course/services/api.md new file mode 100644 index 0000000..53b2c6b --- /dev/null +++ b/apps/course/services/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 +``` +d +### 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) + +
diff --git a/apps/course/views/course.py b/apps/course/views/course.py index e9e8b92..4fa5e1c 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -602,6 +602,15 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): return metadata def _build_live_session_context(self, course: Course) -> dict: + """ + Build live session context with real-time PlugNMeet verification. + + This method: + 1. Finds the latest session for the course + 2. Verifies with PlugNMeet if the room is actually active + 3. Auto-closes sessions if PlugNMeet reports room is inactive + 4. Returns accurate session state independent of webhook delays + """ latest_session = ( CourseLiveSession.objects.filter(course=course) .order_by('-started_at', '-id') @@ -609,6 +618,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): ) if not latest_session: + logger.debug(f"[Live Session Context] No session found for course={course.slug}") return { 'is_online': False, 'live_session': None, @@ -620,6 +630,13 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): started_at = latest_session.started_at ended_at = latest_session.ended_at is_online = bool(started_at and not ended_at) + + # CRITICAL: Verify room status with PlugNMeet if session appears online + # This ensures we don't rely solely on webhooks which may fail or be delayed + if is_online and latest_session.room_id: + is_online = self._verify_and_sync_room_status(latest_session) + # Refresh ended_at in case session was closed + ended_at = latest_session.ended_at live_session_data = { 'id': latest_session.id, @@ -629,13 +646,78 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): 'ended_at': self._format_datetime(ended_at), } - return { + context = { 'is_online': is_online, 'live_session': live_session_data, 'active_room_id': live_session_data['room_id'] if is_online and live_session_data['room_id'] else None, 'livesession_started_at': live_session_data['started_at'], 'livesession_ended_at': live_session_data['ended_at'], } + + logger.debug(f"[Live Session Context] course={course.slug} is_online={is_online} room_id={live_session_data['room_id']}") + return context + + def _verify_and_sync_room_status(self, session: CourseLiveSession) -> bool: + """ + Verify room status with PlugNMeet and sync local database. + + Args: + session: The CourseLiveSession to verify + + Returns: + bool: True if room is active, False if inactive or verification failed + + Side effects: + - Closes session in database if PlugNMeet reports room is inactive + - Updates LiveSessionUser records accordingly + """ + if not session.room_id: + logger.warning(f"[Room Sync] Session has no room_id - session_id={session.id}") + return False + + try: + client = PlugNMeetClient() + response = client.is_room_active(session.room_id) + + # PlugNMeet returns: {"status": true, "msg": "...", "isActive": true/false} + is_active = response.get('isActive', False) + response_msg = response.get('msg', 'unknown') + + if is_active: + logger.debug(f"[Room Sync] ✓ Room verified active - room_id={session.room_id} session_id={session.id} msg={response_msg}") + return True + else: + # Room is not active in PlugNMeet but active in our database + # This happens when: + # 1. Webhook failed to fire + # 2. Room was ended externally + # 3. Room crashed or timed out + logger.warning(f"[Room Sync] ✗ Room inactive in PlugNMeet - auto-closing session_id={session.id} room_id={session.room_id} msg={response_msg}") + self._close_live_session(session) + return False + + except PlugNMeetError as e: + # PlugNMeet API returned an error + error_msg = str(e) + logger.error(f"[Room Sync] PlugNMeet API error - room_id={session.room_id} session_id={session.id} error={error_msg}") + + # Check if error message indicates room doesn't exist + if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower(): + logger.warning(f"[Room Sync] Room not found in PlugNMeet - closing session_id={session.id}") + self._close_live_session(session) + return False + + # For other API errors, assume room might still be active (fail-safe) + logger.warning(f"[Room Sync] Cannot verify room status, assuming inactive for safety - room_id={session.room_id}") + return False + + except Exception as e: + # Network error or unexpected exception + logger.error(f"[Room Sync] Unexpected error verifying room - room_id={session.room_id} session_id={session.id} error={type(e).__name__}: {str(e)}") + # For network errors, fail-safe: assume room might still be active + # but log a warning for monitoring + logger.warning(f"[Room Sync] Network/system error, assuming room inactive for safety") + return False @staticmethod def _user_can_join_live_session(user, course: Course) -> bool: @@ -686,8 +768,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): # except (PlugNMeetError, Exception) as e: # logger.warning(f"[Room Sync] Failed to check room status - room_id={active_session.room_id} error={str(e)}") - @staticmethod - def _close_live_session(session: CourseLiveSession): + def _close_live_session(self, session: CourseLiveSession): """ Close a live session and all related user entries. Sets ended_at for session and exited_at/is_online for users. diff --git a/apps/course/views/live_session.py b/apps/course/views/live_session.py index ee9b51d..732a303 100644 --- a/apps/course/views/live_session.py +++ b/apps/course/views/live_session.py @@ -295,12 +295,21 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): course=course, ended_at__isnull=True ) - logger.info(f"[LiveSession Token] Active session found - session_id={session.id} room_id={session.room_id} course={course_slug}") + logger.info(f"[LiveSession Token] Active session found in DB - session_id={session.id} room_id={session.room_id} course={course_slug}") except CourseLiveSession.DoesNotExist: logger.warning(f"[LiveSession Token] No active session found - course={course_slug} user_id={user.id}") raise AppAPIException({'message': 'No active live session found for this course.'}, status_code=status.HTTP_404_NOT_FOUND) + # CRITICAL: Verify the room is actually active in PlugNMeet before issuing token + # This prevents issuing tokens for rooms that have crashed or ended without webhook notification room_id = session.room_id + if not self._verify_room_is_active(session): + logger.error(f"[LiveSession Token] Room not active in PlugNMeet - refusing token - room_id={room_id} session_id={session.id}") + raise AppAPIException({ + 'status': 'False', + 'message': 'room is not active. create room first', + 'msg': 'room is not active. create room first' + }, status_code=status.HTTP_400_BAD_REQUEST) is_admin = user.can_manage_course(course) user_role = "professor" if is_admin else "student" @@ -368,6 +377,60 @@ class CourseLiveSessionTokenAPIView(GenericAPIView): 'plugnmeet': plugnmeet_response, }) + @staticmethod + def _verify_room_is_active(session: CourseLiveSession) -> bool: + """ + Verify that the room is actually active in PlugNMeet. + + Args: + session: The CourseLiveSession to verify + + Returns: + bool: True if room is active in PlugNMeet, False otherwise + + Side effects: + - Closes session in database if PlugNMeet reports room is inactive + """ + if not session.room_id: + logger.warning(f"[Room Verify] Session has no room_id - session_id={session.id}") + return False + + try: + client = PlugNMeetClient() + response = client.is_room_active(session.room_id) + + is_active = response.get('isActive', False) + response_msg = response.get('msg', 'unknown') + + if is_active: + logger.debug(f"[Room Verify] ✓ Room is active - room_id={session.room_id} session_id={session.id}") + return True + else: + logger.warning(f"[Room Verify] ✗ Room is NOT active - room_id={session.room_id} session_id={session.id} msg={response_msg}") + # Auto-close the session since room is not active + now = timezone.now() + session.ended_at = now + session.save(update_fields=['ended_at', 'updated_at']) + logger.info(f"[Room Verify] Session auto-closed - session_id={session.id} room_id={session.room_id}") + return False + + except PlugNMeetError as e: + error_msg = str(e) + logger.error(f"[Room Verify] PlugNMeet API error - room_id={session.room_id} error={error_msg}") + + # If room not found, close the session + if 'not found' in error_msg.lower() or 'does not exist' in error_msg.lower(): + now = timezone.now() + session.ended_at = now + session.save(update_fields=['ended_at', 'updated_at']) + logger.warning(f"[Room Verify] Room not found - session closed - session_id={session.id}") + + return False + + except Exception as e: + logger.error(f"[Room Verify] Unexpected error - room_id={session.room_id} error={type(e).__name__}: {str(e)}") + return False + @staticmethod def _build_profile_url(request, user): avatar = getattr(user, 'avatar', None)