diff --git a/bots/bot_controller/bot_controller.py b/bots/bot_controller/bot_controller.py index 48bbda88..eec5fc7a 100644 --- a/bots/bot_controller/bot_controller.py +++ b/bots/bot_controller/bot_controller.py @@ -23,6 +23,8 @@ BotMediaRequestMediaTypes, BotMediaRequestStates, BotStates, + ChatMessage, + ChatMessageToOptions, Credentials, MeetingTypes, Participant, @@ -80,6 +82,7 @@ def get_google_meet_bot_adapter(self): wants_any_video_frames_callback=None, add_mixed_audio_chunk_callback=None, upsert_caption_callback=self.closed_caption_manager.upsert_caption, + upsert_chat_message_callback=self.on_new_chat_message, automatic_leave_configuration=self.automatic_leave_configuration, add_encoded_mp4_chunk_callback=None, recording_view=self.bot_in_db.recording_view(), @@ -102,6 +105,7 @@ def get_teams_bot_adapter(self): wants_any_video_frames_callback=None, add_mixed_audio_chunk_callback=None, upsert_caption_callback=self.closed_caption_manager.upsert_caption, + upsert_chat_message_callback=self.on_new_chat_message, automatic_leave_configuration=self.automatic_leave_configuration, add_encoded_mp4_chunk_callback=None, recording_view=self.bot_in_db.recording_view(), @@ -137,6 +141,7 @@ def get_zoom_bot_adapter(self): add_video_frame_callback=self.gstreamer_pipeline.on_new_video_frame, wants_any_video_frames_callback=self.gstreamer_pipeline.wants_any_video_frames, add_mixed_audio_chunk_callback=self.gstreamer_pipeline.on_mixed_audio_raw_data_received_callback, + upsert_chat_message_callback=self.on_new_chat_message, automatic_leave_configuration=self.automatic_leave_configuration, video_frame_size=self.bot_in_db.recording_dimensions(), ) @@ -740,6 +745,41 @@ def save_individual_audio_utterance(self, message): process_utterance.delay(utterance.id) return + def on_new_chat_message(self, chat_message): + GLib.idle_add(lambda: self.upsert_chat_message(chat_message)) + + def upsert_chat_message(self, chat_message): + logger.info(f"Upserting chat message: {chat_message}") + + participant = self.adapter.get_participant(chat_message["participant_uuid"]) + + if participant is None: + logger.warning(f"Warning: No participant found for chat message: {chat_message}") + return + + participant, _ = Participant.objects.get_or_create( + bot=self.bot_in_db, + uuid=participant["participant_uuid"], + defaults={ + "user_uuid": participant["participant_user_uuid"], + "full_name": participant["participant_full_name"], + }, + ) + + ChatMessage.objects.update_or_create( + bot=self.bot_in_db, + source_uuid=chat_message["message_uuid"], + defaults={ + "timestamp": chat_message["timestamp"], + "to": ChatMessageToOptions.ONLY_BOT if chat_message.get("to_bot") else ChatMessageToOptions.EVERYONE, + "text": chat_message["text"], + "participant": participant, + "additional_data": chat_message.get("additional_data", {}), + }, + ) + + return + def on_message_from_adapter(self, message): GLib.idle_add(lambda: self.take_action_based_on_message_from_adapter(message)) diff --git a/bots/bot_pod_creator/bot_pod_creator.py b/bots/bot_pod_creator/bot_pod_creator.py index 46e5a880..eb82de7a 100644 --- a/bots/bot_pod_creator/bot_pod_creator.py +++ b/bots/bot_pod_creator/bot_pod_creator.py @@ -25,7 +25,8 @@ def __init__(self, namespace: str = "attendee"): # Parse instance from version (matches your pattern of {hash}-{timestamp}) self.app_instance = f"{self.app_name}-{self.app_version.split('-')[-1]}" - self.image = f"nduncan{self.app_name}/{self.app_name}:{self.app_version}" + default_pod_image = f"nduncan{self.app_name}/{self.app_name}" + self.image = f"{os.getenv('BOT_POD_IMAGE', default_pod_image)}:{self.app_version}" def create_bot_pod( self, diff --git a/bots/bots_api_urls.py b/bots/bots_api_urls.py index e18d4f4a..3207faa2 100644 --- a/bots/bots_api_urls.py +++ b/bots/bots_api_urls.py @@ -44,6 +44,11 @@ bots_api_views.SpeechView.as_view(), name="bot-speech", ), + path( + "bots//chat_messages", + bots_api_views.ChatMessagesView.as_view(), + name="bot-chat-messages", + ), path( "bots//delete_data", bots_api_views.DeleteDataView.as_view(), diff --git a/bots/bots_api_views.py b/bots/bots_api_views.py index f930053e..4f954dcd 100644 --- a/bots/bots_api_views.py +++ b/bots/bots_api_views.py @@ -10,6 +10,8 @@ extend_schema, ) from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.pagination import CursorPagination from rest_framework.response import Response from rest_framework.views import APIView @@ -24,6 +26,7 @@ BotMediaRequestMediaTypes, BotMediaRequestStates, BotStates, + ChatMessage, Credentials, MediaBlob, MeetingTypes, @@ -33,6 +36,7 @@ from .serializers import ( BotImageSerializer, BotSerializer, + ChatMessageSerializer, CreateBotSerializer, RecordingSerializer, SpeechSerializer, @@ -703,3 +707,92 @@ def get(self, request, object_id): except Bot.DoesNotExist: return Response({"error": "Bot not found"}, status=status.HTTP_404_NOT_FOUND) + + +class ChatMessageCursorPagination(CursorPagination): + ordering = "created_at" + page_size = 25 + + +class ChatMessagesView(GenericAPIView): + authentication_classes = [ApiKeyAuthentication] + pagination_class = ChatMessageCursorPagination + serializer_class = ChatMessageSerializer + + @extend_schema( + operation_id="Get Chat Messages", + summary="Get chat messages sent in the meeting", + description="If the meeting is still in progress, this returns the chat messages sent so far. Results are paginated using cursor pagination.", + responses={ + 200: OpenApiResponse( + response=ChatMessageSerializer(many=True), + description="List of chat messages", + ), + 404: OpenApiResponse(description="Bot not found"), + }, + parameters=[ + *TokenHeaderParameter, + OpenApiParameter( + name="object_id", + type=str, + location=OpenApiParameter.PATH, + description="Bot ID", + examples=[OpenApiExample("Bot ID Example", value="bot_xxxxxxxxxxx")], + ), + OpenApiParameter( + name="updated_after", + type={"type": "string", "format": "ISO 8601 datetime"}, + location=OpenApiParameter.QUERY, + description="Only return chat messages created after this time. Useful when polling for updates.", + required=False, + examples=[OpenApiExample("DateTime Example", value="2024-01-18T12:34:56Z")], + ), + OpenApiParameter( + name="cursor", + type=str, + location=OpenApiParameter.QUERY, + description="Cursor for pagination", + required=False, + ), + ], + tags=["Bots"], + ) + def get(self, request, object_id): + try: + # Get the bot and verify it belongs to the project + bot = Bot.objects.get(object_id=object_id, project=request.auth.project) + + # Get optional updated_after parameter + updated_after = request.query_params.get("updated_after") + + # Query messages for this bot + messages_query = ChatMessage.objects.filter(bot=bot) + + # Filter by updated_after if provided + if updated_after: + try: + updated_after_datetime = parse_datetime(str(updated_after)) + except Exception: + updated_after_datetime = None + + if not updated_after_datetime: + return Response( + {"error": "Invalid updated_after format. Use ISO 8601 format (e.g., 2024-01-18T12:34:56Z)"}, + status=status.HTTP_400_BAD_REQUEST, + ) + messages_query = messages_query.filter(created_at__gt=updated_after_datetime) + + # Apply ordering - now using created_at for cursor pagination + messages = messages_query.order_by("created_at") + + # Let the pagination class handle the rest + page = self.paginate_queryset(messages) + if page is not None: + serializer = self.get_serializer(page, many=True) + return self.get_paginated_response(serializer.data) + + serializer = self.get_serializer(messages, many=True) + return Response(serializer.data) + + except Bot.DoesNotExist: + return Response({"error": "Bot not found"}, status=status.HTTP_404_NOT_FOUND) diff --git a/bots/google_meet_bot_adapter/google_meet_chromedriver_payload.js b/bots/google_meet_bot_adapter/google_meet_chromedriver_payload.js index 977b97f7..fd63e1c1 100644 --- a/bots/google_meet_bot_adapter/google_meet_chromedriver_payload.js +++ b/bots/google_meet_bot_adapter/google_meet_chromedriver_payload.js @@ -442,6 +442,31 @@ const DEVICE_OUTPUT_TYPE = { VIDEO: 2 } +// Chat message manager +class ChatMessageManager { + constructor(ws) { + this.ws = ws; + } + + handleChatMessage(chatMessageRaw) { + try { + const chatMessage = chatMessageRaw.chatMessage; + console.log('handleChatMessage', chatMessage); + + this.ws.sendJson({ + type: 'ChatMessage', + message_uuid: chatMessage.messageId, + participant_uuid: chatMessage.deviceId, + timestamp: Math.floor(chatMessage.timestamp / 1000), + text: chatMessage.chatMessageContent.text, + }); + } + catch (error) { + console.error('Error in handleChatMessage', error); + } + } +} + // User manager class UserManager { constructor(ws) { @@ -1204,6 +1229,7 @@ const captionManager = new CaptionManager(ws); const videoTrackManager = new VideoTrackManager(ws); const styleManager = new StyleManager(); const receiverManager = new ReceiverManager(); +const chatMessageManager = new ChatMessageManager(ws); let rtpReceiverInterceptor = null; if (window.initialData.sendPerParticipantAudio) { rtpReceiverInterceptor = new RTCRtpReceiverInterceptor((receiver, result, ...args) => { @@ -1215,6 +1241,7 @@ window.videoTrackManager = videoTrackManager; window.userManager = userManager; window.styleManager = styleManager; window.receiverManager = receiverManager; +window.chatMessageManager = chatMessageManager; // Create decoders for all message types const messageDecoders = {}; messageTypes.forEach(type => { @@ -1261,7 +1288,9 @@ const handleCollectionEvent = (event) => { const chatMessageWrapper = collectionEvent.body.userInfoListWrapperAndChatWrapperWrapper?.userInfoListWrapperAndChatWrapper?.chatMessageWrapper; if (chatMessageWrapper) { - console.log('chatMessageWrapper', chatMessageWrapper); + for (const chatMessage of chatMessageWrapper) { + window.chatMessageManager?.handleChatMessage(chatMessage); + } } //console.log('deviceOutputInfoList', JSON.stringify(collectionEvent.body.userInfoListWrapperAndChatWrapperWrapper?.deviceInfoWrapper?.deviceOutputInfoList)); diff --git a/bots/migrations/0030_chatmessage.py b/bots/migrations/0030_chatmessage.py new file mode 100644 index 00000000..28669e04 --- /dev/null +++ b/bots/migrations/0030_chatmessage.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.2 on 2025-05-23 04:55 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bots', '0029_recording_transcription_failure_data_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='ChatMessage', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('text', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('to', models.IntegerField(choices=[(1, 'only_bot'), (2, 'everyone')])), + ('timestamp', models.IntegerField()), + ('additional_data', models.JSONField(default=dict)), + ('object_id', models.CharField(editable=False, max_length=32, unique=True)), + ('source_uuid', models.CharField(max_length=255, null=True, unique=True)), + ('bot', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_messages', to='bots.bot')), + ('participant', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='chat_messages', to='bots.participant')), + ], + ), + ] diff --git a/bots/models.py b/bots/models.py index 29820aa5..15ad0f4c 100644 --- a/bots/models.py +++ b/bots/models.py @@ -1486,3 +1486,31 @@ def add_to_response_body_list(self, response_body): self.response_body_list = [response_body] else: self.response_body_list.append(response_body) + + +class ChatMessageToOptions(models.IntegerChoices): + ONLY_BOT = 1, "only_bot" + EVERYONE = 2, "everyone" + + +class ChatMessage(models.Model): + bot = models.ForeignKey(Bot, on_delete=models.CASCADE, related_name="chat_messages") + text = models.TextField() + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + participant = models.ForeignKey(Participant, on_delete=models.CASCADE, related_name="chat_messages") + to = models.IntegerField(choices=ChatMessageToOptions.choices, null=False) + timestamp = models.IntegerField() + additional_data = models.JSONField(null=False, default=dict) + object_id = models.CharField(max_length=32, unique=True, editable=False) + + OBJECT_ID_PREFIX = "msg_" + object_id = models.CharField(max_length=32, unique=True, editable=False) + source_uuid = models.CharField(max_length=255, null=True, unique=True) + + def save(self, *args, **kwargs): + if not self.object_id: + # Generate a random 16-character string + random_string = "".join(random.choices(string.ascii_letters + string.digits, k=16)) + self.object_id = f"{self.OBJECT_ID_PREFIX}{random_string}" + super().save(*args, **kwargs) diff --git a/bots/serializers.py b/bots/serializers.py index c96870af..2996f7fd 100644 --- a/bots/serializers.py +++ b/bots/serializers.py @@ -16,6 +16,7 @@ BotEventSubTypes, BotEventTypes, BotStates, + ChatMessageToOptions, MediaBlob, MeetingTypes, Recording, @@ -733,3 +734,17 @@ def validate_text_to_speech_settings(self, value): raise serializers.ValidationError(e.message) return value + + +class ChatMessageSerializer(serializers.Serializer): + object_id = serializers.CharField() + text = serializers.CharField() + timestamp = serializers.IntegerField() + to = serializers.SerializerMethodField() + sender_name = serializers.CharField(source="participant.full_name") + sender_uuid = serializers.CharField(source="participant.uuid") + sender_user_uuid = serializers.CharField(source="participant.user_uuid", allow_null=True) + additional_data = serializers.JSONField() + + def get_to(self, obj): + return ChatMessageToOptions.choices[obj.to - 1][1] diff --git a/bots/teams_bot_adapter/teams_chromedriver_payload.js b/bots/teams_bot_adapter/teams_chromedriver_payload.js index c2705915..789b3ca6 100644 --- a/bots/teams_bot_adapter/teams_chromedriver_payload.js +++ b/bots/teams_bot_adapter/teams_chromedriver_payload.js @@ -46,6 +46,13 @@ class StyleManager { }); console.log('Bot was removed from meeting, sent notification'); } + + // We need to open the chat window to be able to track messages + const chatButton = document.querySelector('button#chat-button'); + if (chatButton && !this.chatButtonClicked) { + chatButton.click(); + this.chatButtonClicked = true; + } } startSilenceDetection() { @@ -629,6 +636,43 @@ The tracks have a streamId that looks like this mainVideo-39016. The SDP has tha } } + +class ChatMessageManager { + constructor(ws) { + this.ws = ws; + } + + // The more sophisticated approach gets blocked by trusted html csp + stripHtml(html) { + return html.replace(/<[^>]*>/g, ''); + } + + handleChatMessage(chatMessage) { + try { + if (!chatMessage.clientMessageId) + return; + if (!chatMessage.from) + return; + if (!chatMessage.content) + return; + if (!chatMessage.originalArrivalTime) + return; + + const timestamp_ms = new Date(chatMessage.originalArrivalTime).getTime(); + this.ws.sendJson({ + type: 'ChatMessage', + message_uuid: chatMessage.clientMessageId, + participant_uuid: chatMessage.from, + timestamp: Math.floor(timestamp_ms / 1000), + text: this.stripHtml(chatMessage.content), + }); + } + catch (error) { + console.error('Error in handleChatMessage', error); + } + } +} + // User manager class UserManager { constructor(ws) { @@ -1165,6 +1209,9 @@ window.ws = ws; const userManager = new UserManager(ws); window.userManager = userManager; +const chatMessageManager = new ChatMessageManager(ws); +window.chatMessageManager = chatMessageManager; + //const videoTrackManager = new VideoTrackManager(ws); const virtualStreamToPhysicalStreamMappingManager = new VirtualStreamToPhysicalStreamMappingManager(); const dominantSpeakerManager = new DominantSpeakerManager(); @@ -2010,4 +2057,24 @@ navigator.mediaDevices.getUserMedia = function(constraints) { console.error("Error in custom getUserMedia override:", err); throw err; }); - }; \ No newline at end of file + }; + +(function () { + const _bind = Function.prototype.bind; + Function.prototype.bind = function (thisArg, ...args) { + if (this.name === 'onMessageReceived') { + const bound = _bind.apply(this, [thisArg, ...args]); + return function (...callArgs) { + const eventData = callArgs[0]; + if (eventData?.data?.chatServiceBatchEvent?.[0]?.message) + { + const message = eventData.data.chatServiceBatchEvent[0].message; + realConsole?.log('chatMessage', message); + window.chatMessageManager?.handleChatMessage(message); + } + return bound.apply(this, callArgs); + }; + } + return _bind.apply(this, [thisArg, ...args]); + }; + })(); \ No newline at end of file diff --git a/bots/tests/test_zoom_bot.py b/bots/tests/test_zoom_bot.py index d78e1494..c5ea5369 100644 --- a/bots/tests/test_zoom_bot.py +++ b/bots/tests/test_zoom_bot.py @@ -23,6 +23,8 @@ BotMediaRequestMediaTypes, BotMediaRequestStates, BotStates, + ChatMessage, + ChatMessageToOptions, Credentials, CreditTransaction, MediaBlob, @@ -798,6 +800,19 @@ def capture_upload_part(file_path): image_request = None speech_request = None + # Create a mock chat message info object + mock_chat_msg_info = MagicMock() + mock_chat_msg_info.GetContent.return_value = "Hello bot from Test User!" + mock_chat_msg_info.GetSenderUserId.return_value = 2 # Test User's ID + mock_chat_msg_info.GetTimeStamp.return_value = time.time() + mock_chat_msg_info.GetMessageID.return_value = "test_chat_message_id_001" + mock_chat_msg_info.IsChatToAllPanelist.return_value = False + mock_chat_msg_info.IsChatToAll.return_value = False + mock_chat_msg_info.IsChatToWaitingroom.return_value = False + mock_chat_msg_info.IsComment.return_value = False + mock_chat_msg_info.IsThread.return_value = False + mock_chat_msg_info.GetThreadID.return_value = "" + def simulate_join_flow(): nonlocal audio_request, image_request, speech_request @@ -883,6 +898,9 @@ def simulate_join_flow(): # Sleep to give audio output manager time to play the speech audio time.sleep(2.0) + # Simulate chat message received + adapter.on_chat_msg_notification_callback(mock_chat_msg_info, mock_chat_msg_info.GetContent()) + # Simulate meeting ended adapter.meeting_service_event.onMeetingStatusChangedCallback( mock_zoom_sdk_adapter.MEETING_STATUS_ENDED, @@ -998,6 +1016,15 @@ def simulate_join_flow(): self.assertEqual(self.recording.utterances.count(), 1) self.assertIsNotNone(utterance.transcription) + # Verify chat message was processed + chat_messages = ChatMessage.objects.filter(bot=self.bot) + self.assertEqual(chat_messages.count(), 1) + chat_message = chat_messages.first() + self.assertEqual(chat_message.text, "Hello bot from Test User!") + self.assertEqual(chat_message.participant.full_name, "Test User") + self.assertEqual(chat_message.source_uuid, "test_chat_message_id_001") + self.assertEqual(chat_message.to, ChatMessageToOptions.ONLY_BOT) + # Verify the bot adapter received the media controller.adapter.audio_raw_data_sender.send.assert_has_calls( [ diff --git a/bots/web_bot_adapter/web_bot_adapter.py b/bots/web_bot_adapter/web_bot_adapter.py index b5e46170..7faf2391 100644 --- a/bots/web_bot_adapter/web_bot_adapter.py +++ b/bots/web_bot_adapter/web_bot_adapter.py @@ -37,6 +37,7 @@ def __init__( add_mixed_audio_chunk_callback, add_encoded_mp4_chunk_callback, upsert_caption_callback, + upsert_chat_message_callback, automatic_leave_configuration: AutomaticLeaveConfiguration, recording_view: RecordingViews, should_create_debug_recording: bool, @@ -52,6 +53,7 @@ def __init__( self.wants_any_video_frames_callback = wants_any_video_frames_callback self.add_encoded_mp4_chunk_callback = add_encoded_mp4_chunk_callback self.upsert_caption_callback = upsert_caption_callback + self.upsert_chat_message_callback = upsert_chat_message_callback self.start_recording_screen_callback = start_recording_screen_callback self.stop_recording_screen_callback = stop_recording_screen_callback self.recording_view = recording_view @@ -217,6 +219,9 @@ def handle_websocket(self, websocket): self.last_audio_message_processed_time = time.time() self.upsert_caption_callback(json_data["caption"]) + elif json_data.get("type") == "ChatMessage": + self.upsert_chat_message_callback(json_data) + elif json_data.get("type") == "UsersUpdate": for user in json_data["newUsers"]: user["active"] = user["humanized_status"] == "in_meeting" diff --git a/bots/zoom_bot_adapter/zoom_bot_adapter.py b/bots/zoom_bot_adapter/zoom_bot_adapter.py index 1c7bfe09..8f9d79bb 100644 --- a/bots/zoom_bot_adapter/zoom_bot_adapter.py +++ b/bots/zoom_bot_adapter/zoom_bot_adapter.py @@ -82,6 +82,7 @@ def __init__( add_video_frame_callback, wants_any_video_frames_callback, add_mixed_audio_chunk_callback, + upsert_chat_message_callback, automatic_leave_configuration: AutomaticLeaveConfiguration, video_frame_size: tuple[int, int], ): @@ -94,6 +95,7 @@ def __init__( self.add_mixed_audio_chunk_callback = add_mixed_audio_chunk_callback self.add_video_frame_callback = add_video_frame_callback self.wants_any_video_frames_callback = wants_any_video_frames_callback + self.upsert_chat_message_callback = upsert_chat_message_callback self._jwt_token = generate_jwt(zoom_client_id, zoom_client_secret) self.meeting_id, self.meeting_password = parse_join_url(meeting_url) @@ -336,6 +338,29 @@ def on_sharing_status_callback(self, sharing_info): self.active_sharer_source_id = new_active_sharer_source_id self.set_video_input_manager_based_on_state() + def on_chat_msg_notification_callback(self, chat_msg_info, content): + try: + self.upsert_chat_message_callback( + { + "text": chat_msg_info.GetContent(), + "participant_uuid": chat_msg_info.GetSenderUserId(), + "timestamp": chat_msg_info.GetTimeStamp(), + "message_uuid": chat_msg_info.GetMessageID(), + # Simplified logic to determine if the message is for the bot. Not completely accurate. + "to_bot": not chat_msg_info.IsChatToAllPanelist() and not chat_msg_info.IsChatToAll() and not chat_msg_info.IsChatToWaitingroom(), + "additional_data": { + "is_comment": chat_msg_info.IsComment(), + "is_thread": chat_msg_info.IsThread(), + "thread_id": chat_msg_info.GetThreadID(), + "is_chat_to_all": chat_msg_info.IsChatToAll(), + "is_chat_to_all_panelist": chat_msg_info.IsChatToAllPanelist(), + "is_chat_to_waitingroom": chat_msg_info.IsChatToWaitingroom(), + }, + } + ) + except Exception as e: + logger.error(f"Error processing chat message: {e}") + def on_join(self): # Meeting reminder controller self.joined_at = time.time() @@ -352,6 +377,11 @@ def on_join(self): for participant_id in participant_ids_list: self.get_participant(participant_id) + # Chats controller + self.chat_ctrl = self.meeting_service.GetMeetingChatController() + self.chat_ctrl_event = zoom.MeetingChatEventCallbacks(onChatMsgNotificationCallback=self.on_chat_msg_notification_callback) + self.chat_ctrl.SetEvent(self.chat_ctrl_event) + # Meeting sharing controller self.meeting_sharing_controller = self.meeting_service.GetMeetingShareController() self.meeting_share_ctrl_event = zoom.MeetingShareCtrlEventCallbacks(onSharingStatusCallback=self.on_sharing_status_callback) diff --git a/docs/openapi.yml b/docs/openapi.yml index e5bb2f30..3c3ea5c6 100644 --- a/docs/openapi.yml +++ b/docs/openapi.yml @@ -119,6 +119,66 @@ paths: description: Bot details '404': description: Bot not found + /api/v1/bots/{object_id}/chat_messages: + get: + operationId: Get Chat Messages + description: If the meeting is still in progress, this returns the chat messages + sent so far. Results are paginated using cursor pagination. + summary: Get chat messages sent in the meeting + parameters: + - in: header + name: Authorization + schema: + type: string + default: Token YOUR_API_KEY_HERE + description: API key for authentication + required: true + - in: header + name: Content-Type + schema: + type: string + default: application/json + description: Should always be application/json + required: true + - in: query + name: cursor + schema: + type: string + description: Cursor for pagination + - in: path + name: object_id + schema: + type: string + description: Bot ID + required: true + examples: + BotIDExample: + value: bot_xxxxxxxxxxx + summary: Bot ID Example + - in: query + name: updated_after + schema: + type: string + format: ISO 8601 datetime + description: Only return chat messages created after this time. Useful when + polling for updates. + examples: + DateTimeExample: + value: '2024-01-18T12:34:56Z' + summary: DateTime Example + tags: + - Bots + security: + - {} + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PaginatedChatMessageList' + description: List of chat messages + '404': + description: Bot not found /api/v1/bots/{object_id}/delete_data: post: operationId: Delete Bot Data @@ -620,6 +680,35 @@ components: required: - data - type + ChatMessage: + type: object + properties: + object_id: + type: string + text: + type: string + timestamp: + type: integer + to: + type: string + readOnly: true + sender_name: + type: string + sender_uuid: + type: string + sender_user_uuid: + type: string + nullable: true + additional_data: {} + required: + - additional_data + - object_id + - sender_name + - sender_user_uuid + - sender_uuid + - text + - timestamp + - to CreateBotRequest: type: object properties: @@ -791,6 +880,25 @@ components: required: - bot_name - meeting_url + PaginatedChatMessageList: + type: object + required: + - results + properties: + next: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?cursor=cD00ODY%3D" + previous: + type: string + nullable: true + format: uri + example: http://api.example.org/accounts/?cursor=cj0xJnA9NDg3 + results: + type: array + items: + $ref: '#/components/schemas/ChatMessage' Recording: type: object properties: