diff --git a/apps/course/tests/test_live_session_api.py b/apps/course/tests/test_live_session_api.py index 54ccac8..37b2bf4 100644 --- a/apps/course/tests/test_live_session_api.py +++ b/apps/course/tests/test_live_session_api.py @@ -5,6 +5,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile from django.test import override_settings from django.urls import reverse from django.utils import timezone +from dj_language.models import Language from rest_framework import status from rest_framework.test import APITestCase @@ -15,6 +16,7 @@ from apps.course.models import ( CourseLiveSession, Participant, ) +from apps.course.views.course import get_course_slug_value @override_settings( @@ -22,9 +24,19 @@ from apps.course.models import ( PLUGNMEET_API_KEY='test-key', PLUGNMEET_API_SECRET='test-secret', MEDIA_ROOT=tempfile.gettempdir(), + ONLINE_CLASS_FRONTEND_DOMAIN='http://testserver', ) class CourseLiveSessionAPITests(APITestCase): def setUp(self): + Language.objects.update_or_create( + id=69, + defaults={ + 'code': 'en', + 'name': 'English', + 'status': True, + 'countries': [], + }, + ) self.professor = ProfessorUser.objects.create( email='prof@example.com', fullname='Professor Sample', @@ -144,7 +156,12 @@ class CourseLiveSessionAPITests(APITestCase): self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) - def test_validate_metadata_includes_active_room_for_student(self): + @mock.patch('apps.course.views.course.PlugNMeetClient') + def test_validate_metadata_includes_active_room_for_student(self, mock_client_cls): + mock_client = mock_client_cls.return_value + mock_client.is_room_active.return_value = {'status': True, 'msg': 'room is active', 'isActive': True} + mock_client.get_join_token.return_value = {'token': 'joined-student-token'} + session = CourseLiveSession.objects.create( course=self.course, subject='Session Live', @@ -154,7 +171,10 @@ class CourseLiveSessionAPITests(APITestCase): Participant.objects.create(course=self.course, student=self.student) self.client.force_authenticate(user=self.student) - url = reverse('course-online-validate', kwargs={'slug': self.course.slug}) + url = reverse( + 'course-online-validate', + kwargs={'slug': get_course_slug_value(self.course)}, + ) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) @@ -164,8 +184,21 @@ class CourseLiveSessionAPITests(APITestCase): self.assertTrue(metadata['can_join_live_session']) self.assertEqual(metadata['live_session']['room_id'], session.room_id) self.assertIsNotNone(metadata['live_session']['started_at']) + self.assertEqual( + response.data['redirect_path'], + 'http://testserver/?access_token=joined-student-token' + ) + self.assertEqual( + metadata['redirect_path'], + 'http://testserver/?access_token=joined-student-token' + ) + + @mock.patch('apps.course.views.course.PlugNMeetClient') + def test_validate_metadata_for_professor_hides_creation_when_online(self, mock_client_cls): + mock_client = mock_client_cls.return_value + mock_client.is_room_active.return_value = {'status': True, 'msg': 'room is active', 'isActive': True} + mock_client.get_join_token.return_value = {'token': 'joined-prof-token'} - def test_validate_metadata_for_professor_hides_creation_when_online(self): CourseLiveSession.objects.create( course=self.course, subject='Session Live', @@ -174,9 +207,68 @@ class CourseLiveSessionAPITests(APITestCase): ) self.client.force_authenticate(user=self.professor) - url = reverse('course-online-validate', kwargs={'slug': self.course.slug}) + url = reverse( + 'course-online-validate', + kwargs={'slug': get_course_slug_value(self.course)}, + ) response = self.client.get(url) self.assertEqual(response.status_code, status.HTTP_200_OK) metadata = response.data['metadata'] self.assertFalse(metadata['can_create_live_session']) + self.assertEqual( + response.data['redirect_path'], + 'http://testserver/?access_token=joined-prof-token' + ) + + @mock.patch('apps.course.views.course.jwt.encode', return_value='teacher-access-token') + @mock.patch('apps.course.views.course.PlugNMeetClient') + def test_validate_returns_direct_access_token_for_professor_when_class_is_not_online( + self, + mock_client_cls, + _mock_jwt_encode, + ): + mock_client = mock_client_cls.return_value + mock_client.create_room.return_value = {'status': 'success'} + self.client.force_authenticate(user=self.professor) + + url = reverse( + 'course-online-validate', + kwargs={'slug': get_course_slug_value(self.course)}, + ) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + metadata = response.data['metadata'] + self.assertFalse(metadata['is_online']) + self.assertTrue(metadata['can_create_live_session']) + self.assertEqual( + response.data['redirect_path'], + 'http://testserver/?access_token=teacher-access-token', + ) + self.assertEqual(metadata['redirect_path'], response.data['redirect_path']) + self.assertTrue( + CourseLiveSession.objects.filter(course=self.course, ended_at__isnull=True).exists() + ) + + @mock.patch( + 'apps.course.views.course.CourseOnlineClassTokenValidateAPIView._generate_entry_token', + return_value='waiting-token', + ) + def test_validate_returns_waiting_redirect_when_class_is_not_online(self, _mock_generate_entry_token): + Participant.objects.create(course=self.course, student=self.student) + self.client.force_authenticate(user=self.student) + + url = reverse( + 'course-online-validate', + kwargs={'slug': get_course_slug_value(self.course)}, + ) + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + metadata = response.data['metadata'] + self.assertFalse(metadata['is_online']) + self.assertFalse(metadata['can_join_live_session']) + self.assertEqual(response.data['redirect_path'], 'http://testserver/?token=waiting-token&slug=sample-course') + self.assertIn('&slug=sample-course', response.data['redirect_path']) + self.assertEqual(metadata['redirect_path'], response.data['redirect_path']) diff --git a/apps/course/views/course.py b/apps/course/views/course.py index 4096e0f..45af00b 100644 --- a/apps/course/views/course.py +++ b/apps/course/views/course.py @@ -1,8 +1,9 @@ -from django.conf import settings import logging +from typing import Optional from django.conf import settings from django.contrib.auth import get_user_model +from django.core.exceptions import ImproperlyConfigured from django.db.models import Count, Q, F from django.shortcuts import get_object_or_404 from django.utils import timezone @@ -43,6 +44,8 @@ from apps.course.services.plugnmeet import PlugNMeetClient, PlugNMeetError from apps.account.serializers import UserProfileSerializer from utils.exceptions import AppAPIException from utils.redis import OnlineClassTokenManager +import time +import jwt UserModel = get_user_model() @@ -61,6 +64,17 @@ def get_course_slug_value(course: Course) -> str: return str(slug_data or '') +def get_online_class_frontend_base() -> str: + base = getattr( + settings, + "ONLINE_CLASS_FRONTEND_DOMAIN", + getattr(settings, "SITE_DOMAIN", ""), + ).rstrip("/") + if base and not base.startswith(('http://', 'https://')): + base = f"https://{base}" + return base + + class CourseCategoryAPIView(ListAPIView): queryset = CourseCategory.objects.all() serializer_class = CourseCategorySerializer @@ -459,7 +473,7 @@ class CourseOnlineClassTokenAPIView(GenericAPIView): }) # ساخت URL ثابت با token و course slug - frontend_base = getattr(settings, "ONLINE_CLASS_FRONTEND_DOMAIN", getattr(settings, "SITE_DOMAIN", "")).rstrip("/") + frontend_base = get_online_class_frontend_base() course_slug = get_course_slug_value(course) entry_url = f"{frontend_base}/?token={token}&slug={course_slug}" @@ -544,6 +558,8 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): {'user_id': user.id if user.is_authenticated else None, 'extra': {}, 'generated_at': timezone.now().isoformat()}, user=user, ) + redirect_path = self._resolve_redirect_path(request, course, user, metadata) + metadata['redirect_path'] = redirect_path print(f"[Online Validate GET] Success - metadata={metadata}") logger.info(f"[Online Validate GET] Success - user_id={user.id if user.is_authenticated else 'anonymous'} course={slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") @@ -552,6 +568,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): 'course': course_data, 'user': user_data, 'metadata': metadata, + 'redirect_path': redirect_path, }, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -624,6 +641,8 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): course_data = CourseDetailSerializer(course, context={'request': request}).data user_data = UserProfileSerializer(user, context={'request': request}).data metadata = self._build_metadata(course, payload, user=user) + redirect_path = self._resolve_redirect_path(request, course, user, metadata, token_value=token_value) + metadata['redirect_path'] = redirect_path print(f"[Online Validate POST] Success - metadata={metadata}") logger.info(f"[Online Validate POST] Success - user_id={user_id} course={course.slug} can_create={metadata.get('can_create_live_session')} can_join={metadata.get('can_join_live_session')}") @@ -632,6 +651,7 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): 'course': course_data, 'user': user_data, 'metadata': metadata, + 'redirect_path': redirect_path, }, status=status.HTTP_200_OK) def _build_metadata(self, course: Course, payload: dict, user=None) -> dict: @@ -658,12 +678,278 @@ class CourseOnlineClassTokenValidateAPIView(GenericAPIView): 'scheduled_times': timing_data, 'generated_at': payload.get('generated_at'), 'validated_at': timezone.now().isoformat(), - 'redirect_path': payload.get('redirect_path'), + 'redirect_path': None, } metadata.update(live_context) return metadata + def _resolve_redirect_path(self, request, course: Course, user, metadata: dict, token_value: str = None) -> Optional[str]: + if not user or not user.is_authenticated: + return None + + if metadata.get('is_online') and metadata.get('can_join_live_session'): + redirect_path = self._build_live_redirect_path(request, course, user) + if redirect_path: + return redirect_path + + if metadata.get('can_create_live_session'): + redirect_path = self._build_professor_direct_redirect_path(course, user) + if redirect_path: + return redirect_path + + if self._user_has_course_access(user, course): + return self._build_waiting_redirect_path(course, user, token_value=token_value) + + return None + + def _build_waiting_redirect_path(self, course: Course, user, token_value: str = None) -> str: + course_slug = get_course_slug_value(course) + token = token_value or self._generate_entry_token(course, user) + frontend_base = get_online_class_frontend_base() + return f"{frontend_base}/?token={token}&slug={course_slug}" + + def _build_professor_direct_redirect_path(self, course: Course, user) -> Optional[str]: + try: + session = self._ensure_professor_live_session(course) + except (ImproperlyConfigured, PlugNMeetError, Exception) as exc: + logger.warning( + "[Online Validate] Failed to create professor live session - course=%s user_id=%s error=%s", + course.slug, + user.id, + str(exc), + ) + return None + + access_token = self._build_professor_access_token(user, session.room_id) + frontend_base = get_online_class_frontend_base() + return f"{frontend_base}/?access_token={access_token}" + + def _ensure_professor_live_session(self, course: Course) -> CourseLiveSession: + session = ( + CourseLiveSession.objects.filter(course=course, ended_at__isnull=True) + .order_by('-started_at', '-id') + .first() + ) + if session and session.room_id: + return session + + subject = self._get_live_session_subject(course) + room_id = f"room-{course.id}-{int(time.time())}" + session = CourseLiveSession.objects.create( + course=course, + room_id=room_id, + subject=subject, + started_at=timezone.now(), + ) + + client = PlugNMeetClient() + client.create_room({ + 'room_id': room_id, + 'empty_timeout': 90, + 'metadata': self._build_live_room_metadata(subject), + }) + return session + + def _build_professor_access_token(self, user, room_id: str) -> str: + token_payload = { + "room_id": room_id, + "user_info": { + "name": user.get_full_name() or user.email or user.username or f"user-{user.id}", + "user_id": str(user.id), + "is_admin": True, + "is_hidden": False, + }, + } + return jwt.encode( + { + "iss": settings.PLUGNMEET_API_KEY, + "exp": int(time.time()) + 3600, + "sub": str(user.id), + **token_payload, + }, + settings.PLUGNMEET_API_SECRET, + algorithm="HS256", + ) + + @staticmethod + def _get_live_session_subject(course: Course) -> str: + title = "" + course_title = course.title + if isinstance(course_title, list): + for tr in course_title: + if isinstance(tr, dict) and tr.get('language_code') == 'en': + val = tr.get('title') or tr.get('text') or tr.get('value') or tr.get('name') + if val: + title = str(val).strip() + break + if not title: + for tr in course_title: + if isinstance(tr, dict) and tr.get('language_code') == 'ru': + val = tr.get('title') or tr.get('text') or tr.get('value') or tr.get('name') + if val: + title = str(val).strip() + break + if not title: + title = extract_text_from_json(course_title) + if not title: + title = "Course" + return f"{title} Live Session" + + def _build_live_room_metadata(self, subject: str) -> dict: + return { + 'room_title': subject, + 'webhook_url': self._get_plugnmeet_webhook_url(), + 'default_lock_settings': { + 'lock_microphone': True, + 'lock_webcam': True, + 'lock_screen_sharing': True, + 'lock_whiteboard': True, + '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, + 'is_allow_local': False, + 'enable_auto_cloud_recording': False, + 'only_record_admin_webcams': False, + }, + }, + } + + @staticmethod + def _get_plugnmeet_webhook_url() -> str: + base = getattr(settings, 'SITE_DOMAIN', '').rstrip('/') + if not base: + raise ImproperlyConfigured('SITE_DOMAIN must be configured for PlugNMeet webhook delivery.') + if not base.startswith(('http://', 'https://')): + base = f"https://{base}" + return f"{base}/api/courses/plugnmeet/webhook/" + + def _build_live_redirect_path(self, request, course: Course, user) -> Optional[str]: + session = ( + CourseLiveSession.objects.filter(course=course, ended_at__isnull=True) + .order_by('-started_at', '-id') + .first() + ) + if not session or not session.room_id: + return None + + is_admin = bool(user.can_manage_course(course)) + user_info = { + 'user_id': str(user.id), + 'name': user.get_full_name() or user.email or user.username or f"user-{user.id}", + 'is_admin': is_admin, + } + + user_metadata = {} + profile_pic = self._build_profile_url(request, user) + if profile_pic: + user_metadata['profilePic'] = profile_pic + + if not is_admin: + user_metadata['lock_settings'] = { + 'lock_microphone': True, + 'lock_screen_sharing': True, + 'lock_webcam': True, + 'lock_whiteboard': True, + '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 + + if user_metadata: + user_info['user_metadata'] = user_metadata + + try: + client = PlugNMeetClient() + response = client.get_join_token({ + 'room_id': session.room_id, + 'user_info': user_info, + }) + except (PlugNMeetError, Exception) as exc: + logger.warning( + "[Online Validate] Failed to generate direct access token - course=%s user_id=%s error=%s", + course.slug, + user.id, + str(exc), + ) + return None + + access_token = response.get('token') + if not access_token: + return None + + frontend_base = get_online_class_frontend_base() + return f"{frontend_base}/?access_token={access_token}" + + def _generate_entry_token(self, course: Course, user) -> str: + manager = OnlineClassTokenManager() + user_token, _ = Token.objects.get_or_create(user=user) + identifier = f"{user.id}:{user_token.key[:8]}" + token = manager.generate_token(course_id=course.id, user_identifier=identifier) + manager.store_token(token, { + 'course_id': course.id, + 'user_id': user.id, + 'user_token': user_token.key, + 'course_slug': get_course_slug_value(course), + 'extra': { + 'professor_in_class': False, + }, + }) + return token + + @staticmethod + def _user_has_course_access(user, course: Course) -> bool: + if not user or not user.is_authenticated: + return False + if user.is_staff or user.can_manage_course(course): + return True + return Participant.objects.filter(course=course, student=user).exists() + + @staticmethod + def _build_profile_url(request, user): + avatar = getattr(user, 'avatar', None) + if avatar and getattr(avatar, 'url', None): + return request.build_absolute_uri(avatar.url) + return None + def _build_live_session_context(self, course: Course) -> dict: """ Build live session context with real-time PlugNMeet verification. diff --git a/docs/online_class_entry_endpoints_guide.md b/docs/online_class_entry_endpoints_guide.md index 8a9e319..8b9e54d 100644 --- a/docs/online_class_entry_endpoints_guide.md +++ b/docs/online_class_entry_endpoints_guide.md @@ -1,237 +1,149 @@ -# راهنمای استفاده از endpoint های ورود به کلاس آنلاین +# راهنمای جدید ورود به کلاس آنلاین -این فایل برای تیم‌های `frontend` و `flutter` نوشته شده و فقط توضیح می‌دهد: +این فایل برای تیم‌های `frontend` و `flutter` نوشته شده و توضیح می‌دهد که از این به بعد +برای ورود کاربر به کلاس آنلاین، endpoint اصلی فقط `validate` است. -- هر endpoint چه کاری انجام می‌دهد -- در چه شرایطی باید از آن استفاده شود -- ترتیب درست استفاده از endpoint ها چیست +## هدف تغییر -این راهنما وارد کدنویسی React یا Flutter نمی‌شود و فقط منطق استفاده را توضیح می‌دهد. +قبلاً کلاینت برای ورود به کلاس باید بین چند endpoint تصمیم می‌گرفت: -## هدف کلی +- `online/validate` +- `online/token` +- `online/room/token` -کاربر برای ورود به کلاس آنلاین دو حالت دارد: +این کار باعث می‌شد منطق تصمیم‌گیری، ساخت token و تشخیص مسیر redirect در کلاینت پخش شود. -1. کلاس هنوز توسط استاد شروع نشده است -2. کلاس از قبل شروع شده و room فعال است +الان این تصمیم‌گیری به بکند منتقل شده است. -رفتار درست فرانت باید بر اساس همین دو حالت تعیین شود. +## endpoint اصلی -## اصل مهم - -فرانت نباید با درخواست اشتباه باعث ساختن کلاس توسط دانشجو شود. - -بنابراین: - -- اگر کلاس شروع نشده باشد، کاربر باید وارد `waiting page` شود -- اگر کلاس شروع شده باشد، کاربر باید مستقیم `join token` واقعی بگیرد و وارد کلاس شود - -## endpoint ها - -### 1. بررسی وضعیت کلاس - -#### Endpoint +### بررسی وضعیت کلاس و گرفتن مسیر نهایی ورود `GET /api/courses//online/validate/` -#### کاربرد - -این endpoint برای تصمیم‌گیری اولیه فرانت است. - -با این endpoint می‌توان فهمید: - -- آیا کلاس الان آنلاین است یا نه -- آیا کاربر اجازه ورود به کلاس را دارد یا نه -- آیا کاربر استاد است و می‌تواند کلاس را شروع کند یا نه - -#### خروجی مهم - -فیلدهای مهم در `metadata`: - -- `is_online` -- `can_join_live_session` -- `can_create_live_session` -- `has_finished` - -#### زمان استفاده - -این endpoint باید قبل از تصمیم نهایی برای ورود به کلاس صدا زده شود. - -#### تصمیم‌گیری بر اساس پاسخ - -- اگر `is_online = true` و `can_join_live_session = true` - فرانت باید مستقیم به سراغ گرفتن `join token` واقعی برود - -- اگر `is_online = false` - فرانت نباید مستقیم سراغ `room/token` برود و باید از flow صفحه انتظار استفاده کند - -- اگر `can_create_live_session = true` -کاربر استاد است و فرانت باید مستقیم به سراغ گرفتن `join token` واقعی برود - ---- - -### 2. ساخت لینک ورود موقت برای waiting page - -#### Endpoint - -`POST /api/courses//online/token/` +## کاری که این endpoint انجام می‌دهد -#### کاربرد +این endpoint حالا همه این کارها را یکجا انجام می‌دهد: -این endpoint برای ورود به waiting flow استفاده می‌شود. +- وضعیت کلاس را بررسی می‌کند +- مشخص می‌کند کاربر اجازه ورود دارد یا نه +- اگر کلاس شروع شده باشد، `access_token` مستقیم کلاس را می‌سازد +- اگر کلاس شروع نشده باشد، `temporary token` صفحه انتظار را می‌سازد +- مسیر نهایی redirect را در `redirect_path` برمی‌گرداند -این endpoint: +## منطق پاسخ -- یک `temporary token` می‌سازد -- یک URL ورود به `conference_client` برمی‌گرداند +### اگر کلاس آنلاین باشد -#### چه زمانی باید استفاده شود +بکند: -فقط وقتی که کلاس هنوز شروع نشده است. +- `join token` واقعی PlugNMeet را می‌سازد +- کاربر را باید به مسیر مستقیم کلاس هدایت کرد -#### چه زمانی نباید استفاده شود +نمونه: -اگر کلاس از قبل شروع شده و room فعال است، نباید این endpoint مسیر اصلی ورود باشد. +`https://meet.example.com/?access_token=` -در آن حالت باید مستقیم `join token` واقعی گرفته شود. +### اگر کلاس هنوز آنلاین نشده باشد -#### ورودی +بکند: -- `course_id` در URL -- هدر احراز هویت کاربر +- `temporary token` صفحه انتظار را می‌سازد +- کاربر را باید به flow انتظار/پری‌جوین هدایت کرد -بدنه عملا می‌تواند خالی باشد. +نمونه: -#### خروجی +`https://meet.example.com/?token=&slug=` -پاسخ شامل این فیلدهاست: +### اگر کلاس هنوز آنلاین نشده باشد و کاربر استاد باشد -- `token` -- `url` -- `expires_in` +بکند: +- room را همان لحظه می‌سازد +- `access_token` مستقیم ورود به کلاس را برمی‌گرداند +- استاد مستقیم وارد خود کلاس می‌شود -### 3. گرفتن join token واقعی برای ورود مستقیم به کلاس +نمونه: -#### Endpoint +`https://meet.example.com/?access_token=` -`POST /api/courses/online/room/token/` +## فیلد مهم جدید -#### کاربرد +در پاسخ این endpoint، فیلد `redirect_path` برگردانده می‌شود. -این endpoint `access_token` واقعی PlugNMeet را می‌سازد. +کلاینت باید فقط از همین فیلد برای navigation استفاده کند. -با این token کاربر مستقیم وارد کلاس می‌شود. +## ساختار پاسخ -#### چه زمانی باید استفاده شود +پاسخ همچنان شامل این بخش‌هاست: -وقتی که کلاس از قبل شروع شده و room فعال است. +- `course` +- `user` +- `metadata` +- `redirect_path` -#### چه زمانی نباید استفاده شود +فیلد `metadata.redirect_path` هم با همین مقدار نهایی پر می‌شود تا سازگاری قبلی حفظ شود. -وقتی هنوز کلاس شروع نشده است. +## رفتار پیشنهادی کلاینت -در آن حالت این endpoint مسیر مناسب ورود نیست و باید از waiting flow استفاده شود. +### سناریوی استاندارد -#### ورودی +1. کاربر روی دکمه ورود به کلاس می‌زند +2. کلاینت فقط `online/validate` را صدا می‌زند +3. اگر `redirect_path` مقدار داشت: + - کاربر به همان مسیر هدایت می‌شود +4. اگر `redirect_path` مقدار نداشت: + - یعنی کاربر اجازه ورود ندارد یا هنوز شرایط ورود برای او مهیا نیست -بدنه: +## معنی فیلدهای مهم metadata -`course_slug` - -به همراه هدر احراز هویت کاربر - -#### خروجی - -پاسخ شامل: - -- `room_id` -- `token` - -#### رفتار درست بعد از دریافت پاسخ - -کاربر باید مستقیم به `conference_client` با `access_token` هدایت شود. - -مثال: - -`https://meet.imamjavad.online/?access_token=` - - -## ترتیب درست استفاده در فرانت - -### سناریو 1: کاربر روی دکمه ورود به کلاس می‌زند - -ترتیب درست: - -1. فرانت وضعیت کلاس را با `online/validate` بررسی کند -2. اگر کلاس فعال بود: - - `online/room/token/` - - هدایت مستقیم با `access_token` -3. اگر کلاس فعال نبود: - - `online/token/` - - هدایت کاربر به waiting page - -## منطق تصمیم‌گیری پیشنهادی - -- `is_online = true` - مسیر درست: ورود مستقیم به کلاس - -- `is_online = false` - مسیر درست: waiting page - -## رفتار داخل conference_client - -`conference_client` الان دو ورودی را می‌فهمد: - -### حالت اول: ورود مستقیم - -اگر URL شامل این باشد: +- `is_online` +- `can_join_live_session` +- `can_create_live_session` +- `has_finished` +- `redirect_path` -- `access_token` +## نکته مهم برای صفحه انتظار -کاربر مستقیم وارد flow اصلی کلاس می‌شود. +اگر `redirect_path` از نوع `?token=...&slug=...` باشد: -### حالت دوم: waiting flow +- کاربر وارد `conference_client` می‌شود +- صفحه انتظار یا pre-join نمایش داده می‌شود +- اگر استاد باشد، می‌تواند از همان flow کلاس را شروع کند +- اگر دانشجو باشد، منتظر شروع کلاس می‌ماند -اگر URL شامل این باشد: +اگر کاربر استاد باشد و کلاس هنوز شروع نشده باشد: -- `token` -- `slug` +- `redirect_path` باید مستقیم از نوع `?access_token=...` باشد +- کلاینت نباید استاد را به waiting page بفرستد -کاربر وارد waiting/pre-join flow می‌شود. +## نکته مهم برای ورود مستقیم -در این حالت: +اگر `redirect_path` از نوع `?access_token=...` باشد: -1. temporary token به auth token واقعی تبدیل می‌شود -2. وضعیت کلاس از Django خوانده می‌شود -3. اگر کلاس هنوز شروع نشده باشد، waiting page نمایش داده می‌شود -4. اگر کلاس شروع شده باشد، دکمه ورود به کلاس نمایش داده می‌شود -5. اگر کاربر استاد باشد و کلاس شروع نشده باشد، دکمه شروع کلاس نمایش داده می‌شود +- کاربر مستقیم وارد کلاس می‌شود +- دیگر نیازی نیست کلاینت جداگانه `room/token` را صدا بزند -## آپدیت خودکار وضعیت +## وضعیت endpoint های قبلی -در waiting flow، وضعیت کلاس به صورت polling دوره‌ای از Django بررسی می‌شود. +endpoint های زیر هنوز ممکن است برای سازگاری یا استفاده داخلی موجود باشند: -بنابراین: +- `POST /api/courses//online/token/` +- `POST /api/courses/online/room/token/` -- اگر استاد کلاس را شروع کند، صفحه انتظار به‌روزرسانی می‌شود -- اگر استاد کلاس را ببندد، وضعیت صفحه دوباره به حالت انتظار/پایان بازمی‌گردد +اما برای flow اصلی ورود کاربر، کلاینت جدید نباید روی آن‌ها تصمیم‌گیری انجام دهد. ## جمع‌بندی نهایی -### برای فرانت اصلی سایت یا اپ - -- ابتدا همیشه `online/validate` -- اگر کلاس فعال بود: `online/room/token/` -- اگر کلاس فعال نبود: `online/token/` - -### برای conference_client +برای ورود کاربر به کلاس: -- اگر `access_token` داشت: ورود مستقیم -- اگر `token + slug` داشت: waiting flow +- فقط `online/validate` را صدا بزنید +- فقط `redirect_path` را بخوانید +- بر اساس آن redirect کنید -### قاعده مهم +یعنی منطق انتخاب بین: -`online/token/` برای قبل از شروع کلاس است +- صفحه انتظار +- ورود مستقیم به کلاس -`online/room/token/` برای وقتی است که کلاس واقعا شروع شده است +دیگر مسئولیت کلاینت نیست و در بکند انجام می‌شود.