{"version":3,"sources":["src/common.speech/Transcription/ConversationServiceAdapter.ts"],"names":[],"mappings":"AAGA,OAAO,EAIH,YAAY,EACZ,WAAW,EAEd,MAAM,yBAAyB,CAAC;AACjC,OAAO,EACH,qBAAqB,EACrB,kBAAkB,EAMrB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEH,eAAe,EACf,kBAAkB,EAClB,gBAAgB,EAChB,qBAAqB,EACxB,MAAM,eAAe,CAAC;AAgBvB,OAAO,EAAE,gCAAgC,EAAE,MAAM,uCAAuC,CAAC;AAYzF;;GAEG;AACH,qBAAa,0BAA2B,SAAQ,qBAAqB;IACjE,OAAO,CAAC,gCAAgC,CAAmC;IAC3E,OAAO,CAAC,iCAAiC,CAAqB;IAC9D,OAAO,CAAC,gCAAgC,CAAS;IACjD,OAAO,CAAC,8BAA8B,CAAkB;IACxD,OAAO,CAAC,8BAA8B,CAA6B;IACnE,OAAO,CAAC,2BAA2B,CAAmC;IACtE,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,oBAAoB,CAAU;IACtC,OAAO,CAAC,0BAA0B,CAAS;IAC3C,OAAO,CAAC,0BAA0B,CAAU;gBAGxC,cAAc,EAAE,eAAe,EAC/B,iBAAiB,EAAE,kBAAkB,EACrC,WAAW,EAAE,YAAY,EACzB,gBAAgB,EAAE,gBAAgB,EAClC,4BAA4B,EAAE,gCAAgC;IAiB3D,UAAU,IAAI,OAAO;IAIf,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IASvC,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO3C,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAM7D,SAAS,CAAC,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;cAezB,2BAA2B,IAAI,OAAO,CAAC,OAAO,CAAC;IAK/D,SAAS,CAAC,iBAAiB,CACvB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,kBAAkB,EAAE,kBAAkB,EACtC,SAAS,EAAE,qBAAqB,EAChC,KAAK,EAAE,MAAM,GAAG,IAAI;IAoBxB;;OAEG;cACa,uBAAuB,CAAC,UAAU,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,WAAW,CAAC;IAK/F;;OAEG;YACW,kCAAkC;YA0VlC,gBAAgB;IAkB9B,OAAO,CAAC,gBAAgB;IA2BxB,OAAO,CAAC,eAAe;CAY1B","file":"ConversationServiceAdapter.d.ts","sourcesContent":["// Copyright (c) Microsoft Corporation. All rights reserved.\n// Licensed under the MIT license.\n\nimport {\n    ConnectionState,\n    createNoDashGuid,\n    Deferred,\n    IAudioSource,\n    IConnection,\n    MessageType,\n} from \"../../common/Exports.js\";\nimport {\n    CancellationErrorCode,\n    CancellationReason,\n    ConversationExpirationEventArgs,\n    ConversationTranslationCanceledEventArgs,\n    ConversationTranslationResult,\n    ResultReason,\n    Translations\n} from \"../../sdk/Exports.js\";\nimport {\n    CognitiveTokenAuthentication,\n    IAuthentication,\n    IConnectionFactory,\n    RecognizerConfig,\n    ServiceRecognizerBase\n} from \"../Exports.js\";\nimport { ConversationConnectionMessage } from \"./ConversationConnectionMessage.js\";\nimport { ConversationRequestSession } from \"./ConversationRequestSession.js\";\nimport {\n    ConversationReceivedTranslationEventArgs,\n    LockRoomEventArgs,\n    MuteAllEventArgs,\n    ParticipantAttributeEventArgs,\n    ParticipantEventArgs,\n    ParticipantsListEventArgs\n} from \"./ConversationTranslatorEventArgs.js\";\nimport {\n    ConversationTranslatorCommandTypes,\n    ConversationTranslatorMessageTypes,\n    IInternalParticipant\n} from \"./ConversationTranslatorInterfaces.js\";\nimport { ConversationTranslatorRecognizer } from \"./ConversationTranslatorRecognizer.js\";\nimport {\n    CommandResponsePayload,\n    IParticipantPayloadResponse,\n    IParticipantsListPayloadResponse,\n    ITranslationResponsePayload,\n    ParticipantPayloadResponse,\n    ParticipantsListPayloadResponse,\n    SpeechResponsePayload,\n    TextResponsePayload\n} from \"./ServiceMessages/Exports.js\";\n\n/**\n * The service adapter handles sending and receiving messages to the Conversation Translator websocket.\n */\nexport class ConversationServiceAdapter extends ServiceRecognizerBase {\n    private privConversationServiceConnector: ConversationTranslatorRecognizer;\n    private privConversationConnectionFactory: IConnectionFactory;\n    private privConversationAuthFetchEventId: string;\n    private privConversationAuthentication: IAuthentication;\n    private privConversationRequestSession: ConversationRequestSession;\n    private privConnectionConfigPromise: Promise<IConnection> = undefined;\n    private privConnectionLoop: Promise<void>;\n    private terminateMessageLoop: boolean;\n    private privLastPartialUtteranceId: string;\n    private privConversationIsDisposed: boolean;\n\n    public constructor(\n        authentication: IAuthentication,\n        connectionFactory: IConnectionFactory,\n        audioSource: IAudioSource,\n        recognizerConfig: RecognizerConfig,\n        conversationServiceConnector: ConversationTranslatorRecognizer) {\n\n        super(authentication, connectionFactory, audioSource, recognizerConfig, conversationServiceConnector);\n\n        this.privLastPartialUtteranceId = \"\";\n        this.privConversationServiceConnector = conversationServiceConnector;\n        this.privConversationAuthentication = authentication;\n        this.receiveMessageOverride = (): Promise<void> => this.receiveConversationMessageOverride();\n        this.recognizeOverride = (): Promise<void> => this.noOp();\n        this.postConnectImplOverride = (connection: Promise<IConnection>): Promise<IConnection> => this.conversationConnectImpl(connection);\n        this.configConnectionOverride = (): Promise<IConnection> => this.configConnection();\n        this.disconnectOverride = (): Promise<void> => this.privDisconnect();\n        this.privConversationRequestSession = new ConversationRequestSession(createNoDashGuid());\n        this.privConversationConnectionFactory = connectionFactory;\n        this.privConversationIsDisposed = false;\n    }\n\n    public isDisposed(): boolean {\n        return super.isDisposed() || this.privConversationIsDisposed;\n    }\n\n    public async dispose(reason?: string): Promise<void> {\n        this.privConversationIsDisposed = true;\n        if (this.privConnectionConfigPromise !== undefined) {\n            const connection: IConnection = await this.privConnectionConfigPromise;\n            await connection.dispose(reason);\n        }\n        await super.dispose(reason);\n    }\n\n    public async sendMessage(message: string): Promise<void> {\n        const connection: IConnection = await this.fetchConnection();\n        return connection.send(new ConversationConnectionMessage(\n            MessageType.Text,\n            message));\n    }\n\n    public async sendMessageAsync(message: string): Promise<void> {\n        const connection: IConnection = await this.fetchConnection();\n\n        await connection.send(new ConversationConnectionMessage(MessageType.Text, message));\n    }\n\n    protected privDisconnect(): Promise<void> {\n        if (this.terminateMessageLoop) {\n            return;\n        }\n        this.cancelRecognition(this.privConversationRequestSession.sessionId,\n            this.privConversationRequestSession.requestId,\n            CancellationReason.Error,\n            CancellationErrorCode.NoError,\n            \"Disconnecting\");\n\n        this.terminateMessageLoop = true;\n        return Promise.resolve();\n    }\n\n    // eslint-disable-next-line @typescript-eslint/require-await\n    protected async processTypeSpecificMessages(): Promise<boolean> {\n        return true;\n    }\n\n    // Cancels recognition.\n    protected cancelRecognition(\n        sessionId: string,\n        requestId: string,\n        cancellationReason: CancellationReason,\n        errorCode: CancellationErrorCode,\n        error: string): void {\n\n        this.terminateMessageLoop = true;\n\n        const cancelEvent: ConversationTranslationCanceledEventArgs = new ConversationTranslationCanceledEventArgs(\n            cancellationReason,\n            error,\n            errorCode,\n            undefined,\n            sessionId);\n\n        try {\n            if (!!this.privConversationServiceConnector.canceled) {\n                this.privConversationServiceConnector.canceled(this.privConversationServiceConnector, cancelEvent);\n            }\n        } catch {\n            // continue on error\n        }\n    }\n\n    /**\n     * Establishes a websocket connection to the end point.\n     */\n    protected async conversationConnectImpl(connection: Promise<IConnection>): Promise<IConnection> {\n        this.privConnectionLoop = this.startMessageLoop();\n        return connection;\n    }\n\n    /**\n     * Process incoming websocket messages\n     */\n    private async receiveConversationMessageOverride(): Promise<void> {\n        if (this.isDisposed() || this.terminateMessageLoop) {\n            return Promise.resolve();\n        }\n        // we won't rely on the cascading promises of the connection since we want to continually be available to receive messages\n        const communicationCustodian: Deferred<void> = new Deferred<void>();\n\n        try {\n            const connection: IConnection = await this.fetchConnection();\n            const message: ConversationConnectionMessage = await connection.read() as ConversationConnectionMessage;\n            if (this.isDisposed() || this.terminateMessageLoop) {\n                // We're done.\n                communicationCustodian.resolve();\n                return Promise.resolve();\n            }\n\n            if (!message) {\n                return this.receiveConversationMessageOverride();\n            }\n\n            const sessionId: string = this.privConversationRequestSession.sessionId;\n            const conversationMessageType: string = message.conversationMessageType.toLowerCase();\n            let sendFinal: boolean = false;\n\n            try {\n                switch (conversationMessageType) {\n                    case \"info\":\n                    case \"participant_command\":\n                    case \"command\":\n                        const commandPayload: CommandResponsePayload = CommandResponsePayload.fromJSON(message.textBody);\n                        switch (commandPayload.command.toLowerCase()) {\n\n                            /**\n                             * 'ParticpantList' is the first message sent to the user after the websocket connection has opened.\n                             * The consuming client must wait for this message to arrive\n                             * before starting to send their own data.\n                             */\n                            case \"participantlist\":\n\n                                const participantsPayload: IParticipantsListPayloadResponse = ParticipantsListPayloadResponse.fromJSON(message.textBody);\n\n                                const participantsResult: IInternalParticipant[] = participantsPayload.participants.map((p: IParticipantPayloadResponse): IInternalParticipant => {\n                                    const participant: IInternalParticipant = {\n                                        avatar: p.avatar,\n                                        displayName: p.nickname,\n                                        id: p.participantId,\n                                        isHost: p.ishost,\n                                        isMuted: p.ismuted,\n                                        isUsingTts: p.usetts,\n                                        preferredLanguage: p.locale\n                                    };\n                                    return participant;\n                                });\n\n                                if (!!this.privConversationServiceConnector.participantsListReceived) {\n                                    this.privConversationServiceConnector.participantsListReceived(this.privConversationServiceConnector,\n                                        new ParticipantsListEventArgs(participantsPayload.roomid, participantsPayload.token,\n                                            participantsPayload.translateTo, participantsPayload.profanityFilter,\n                                            participantsPayload.roomProfanityFilter, participantsPayload.roomLocked,\n                                            participantsPayload.muteAll, participantsResult, sessionId));\n                                }\n                                break;\n\n                            /**\n                             * 'SetTranslateToLanguages' represents the list of languages being used in the Conversation by all users(?).\n                             * This is sent at the start of the Conversation\n                             */\n                            case \"settranslatetolanguages\":\n\n                                if (!!this.privConversationServiceConnector.participantUpdateCommandReceived) {\n                                    this.privConversationServiceConnector.participantUpdateCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantAttributeEventArgs(commandPayload.participantId,\n                                            ConversationTranslatorCommandTypes.setTranslateToLanguages,\n                                            commandPayload.value, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'SetProfanityFiltering' lets the client set the level of profanity filtering.\n                             * If sent by the participant the setting will effect only their own profanity level.\n                             * If sent by the host, the setting will effect all participants including the host.\n                             * Note: the profanity filters differ from Speech Service (?): 'marked', 'raw', 'removed', 'tagged'\n                             */\n                            case \"setprofanityfiltering\":\n\n                                if (!!this.privConversationServiceConnector.participantUpdateCommandReceived) {\n                                    this.privConversationServiceConnector.participantUpdateCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantAttributeEventArgs(commandPayload.participantId,\n                                            ConversationTranslatorCommandTypes.setProfanityFiltering,\n                                            commandPayload.value, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'SetMute' is sent if the participant has been muted by the host.\n                             * Check the 'participantId' to determine if the current user has been muted.\n                             */\n                            case \"setmute\":\n\n                                if (!!this.privConversationServiceConnector.participantUpdateCommandReceived) {\n                                    this.privConversationServiceConnector.participantUpdateCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantAttributeEventArgs(commandPayload.participantId,\n                                            ConversationTranslatorCommandTypes.setMute,\n                                            commandPayload.value, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'SetMuteAll' is sent if the Conversation has been muted by the host.\n                             */\n                            case \"setmuteall\":\n\n                                if (!!this.privConversationServiceConnector.muteAllCommandReceived) {\n                                    this.privConversationServiceConnector.muteAllCommandReceived(this.privConversationServiceConnector,\n                                        new MuteAllEventArgs(commandPayload.value as boolean, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'RoomExpirationWarning' is sent towards the end of the Conversation session to give a timeout warning.\n                             */\n                            case \"roomexpirationwarning\":\n\n                                if (!!this.privConversationServiceConnector.conversationExpiration) {\n                                    this.privConversationServiceConnector.conversationExpiration(this.privConversationServiceConnector,\n                                        new ConversationExpirationEventArgs(commandPayload.value as number, this.privConversationRequestSession.sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'SetUseTts' is sent as a confirmation if the user requests TTS to be turned on or off.\n                             */\n                            case \"setusetts\":\n\n                                if (!!this.privConversationServiceConnector.participantUpdateCommandReceived) {\n                                    this.privConversationServiceConnector.participantUpdateCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantAttributeEventArgs(commandPayload.participantId,\n                                            ConversationTranslatorCommandTypes.setUseTTS,\n                                            commandPayload.value, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'SetLockState' is set if the host has locked or unlocked the Conversation.\n                             */\n                            case \"setlockstate\":\n\n                                if (!!this.privConversationServiceConnector.lockRoomCommandReceived) {\n                                    this.privConversationServiceConnector.lockRoomCommandReceived(this.privConversationServiceConnector,\n                                        new LockRoomEventArgs(commandPayload.value as boolean, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'ChangeNickname' is received if a user changes their display name.\n                             * Any cached particpiants list should be updated to reflect the display name.\n                             */\n                            case \"changenickname\":\n\n                                if (!!this.privConversationServiceConnector.participantUpdateCommandReceived) {\n                                    this.privConversationServiceConnector.participantUpdateCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantAttributeEventArgs(commandPayload.participantId,\n                                            ConversationTranslatorCommandTypes.changeNickname,\n                                            commandPayload.value, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'JoinSession' is sent when a user joins the Conversation.\n                             */\n                            case \"joinsession\":\n\n                                const joinParticipantPayload: ParticipantPayloadResponse = ParticipantPayloadResponse.fromJSON(message.textBody);\n\n                                const joiningParticipant: IInternalParticipant = {\n                                    avatar: joinParticipantPayload.avatar,\n                                    displayName: joinParticipantPayload.nickname,\n                                    id: joinParticipantPayload.participantId,\n                                    isHost: joinParticipantPayload.ishost,\n                                    isMuted: joinParticipantPayload.ismuted,\n                                    isUsingTts: joinParticipantPayload.usetts,\n                                    preferredLanguage: joinParticipantPayload.locale,\n                                };\n\n                                if (!!this.privConversationServiceConnector.participantJoinCommandReceived) {\n                                    this.privConversationServiceConnector.participantJoinCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantEventArgs(\n                                            joiningParticipant,\n                                            sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'LeaveSession' is sent when a user leaves the Conversation'.\n                             */\n                            case \"leavesession\":\n\n                                const leavingParticipant: IInternalParticipant = {\n                                    id: commandPayload.participantId\n                                };\n\n                                if (!!this.privConversationServiceConnector.participantLeaveCommandReceived) {\n                                    this.privConversationServiceConnector.participantLeaveCommandReceived(this.privConversationServiceConnector,\n                                        new ParticipantEventArgs(leavingParticipant, sessionId));\n                                }\n\n                                break;\n\n                            /**\n                             * 'DisconnectSession' is sent when a user is disconnected from the session (e.g. network problem).\n                             * Check the 'ParticipantId' to check whether the message is for the current user.\n                             */\n                            case \"disconnectsession\":\n\n                                // eslint-disable-next-line @typescript-eslint/no-unused-vars\n                                const disconnectParticipant: IInternalParticipant = {\n                                    id: commandPayload.participantId\n                                };\n\n                                break;\n\n                            case \"token\":\n                                const token = new CognitiveTokenAuthentication(\n                                    (): Promise<string> => {\n                                        const authorizationToken = commandPayload.token;\n                                        return Promise.resolve(authorizationToken);\n                                    },\n                                    (): Promise<string> => {\n                                        const authorizationToken = commandPayload.token;\n                                        return Promise.resolve(authorizationToken);\n                                    });\n                                this.authentication = token;\n                                this.privConversationServiceConnector.onToken(token);\n\n                                break;\n\n                            /**\n                             * Message not recognized.\n                             */\n                            default:\n                                break;\n                        }\n                        break;\n\n                    /**\n                     * 'partial' (or 'hypothesis') represents a unfinalized speech message.\n                     */\n                    case \"partial\":\n\n                    /**\n                     * 'final' (or 'phrase') represents a finalized speech message.\n                     */\n                    case \"final\":\n\n                        const speechPayload: SpeechResponsePayload = SpeechResponsePayload.fromJSON(message.textBody);\n                        const conversationResultReason: ResultReason = (conversationMessageType === \"final\") ? ResultReason.TranslatedParticipantSpeech : ResultReason.TranslatingParticipantSpeech;\n\n                        const speechResult: ConversationTranslationResult = new ConversationTranslationResult(speechPayload.participantId,\n                            this.getTranslations(speechPayload.translations),\n                            speechPayload.language,\n                            speechPayload.id,\n                            conversationResultReason,\n                            speechPayload.recognition,\n                            undefined,\n                            undefined,\n                            message.textBody,\n                            undefined);\n\n                        if (speechPayload.isFinal) {\n                            // check the length, sometimes empty finals are returned\n                            if (speechResult.text !== undefined && speechResult.text.length > 0) {\n                                sendFinal = true;\n                            } else if (speechPayload.id === this.privLastPartialUtteranceId) {\n                                // send final as normal. We had a non-empty partial for this same utterance\n                                // so sending the empty final is important\n                                sendFinal = true;\n                            } else {\n                                // suppress unneeded final\n                            }\n\n                            if (sendFinal) {\n                                if (!!this.privConversationServiceConnector.translationReceived) {\n                                    this.privConversationServiceConnector.translationReceived(this.privConversationServiceConnector,\n                                        new ConversationReceivedTranslationEventArgs(ConversationTranslatorMessageTypes.final, speechResult, sessionId));\n                                }\n                            }\n                        } else if (speechResult.text !== undefined) {\n                            this.privLastPartialUtteranceId = speechPayload.id;\n                            if (!!this.privConversationServiceConnector.translationReceived) {\n                                this.privConversationServiceConnector.translationReceived(this.privConversationServiceConnector,\n                                    new ConversationReceivedTranslationEventArgs(ConversationTranslatorMessageTypes.partial, speechResult, sessionId));\n                            }\n                        }\n\n                        break;\n\n                    /**\n                     * \"translated_message\" is a text message or instant message (IM).\n                     */\n                    case \"translated_message\":\n\n                        const textPayload: TextResponsePayload = TextResponsePayload.fromJSON(message.textBody);\n                        // TODO: (Native parity) a result reason should be set based whether the participantId is ours or not\n\n                        const textResult: ConversationTranslationResult = new ConversationTranslationResult(textPayload.participantId,\n                            this.getTranslations(textPayload.translations),\n                            textPayload.language,\n                            undefined,\n                            undefined,\n                            textPayload.originalText,\n                            undefined,\n                            undefined,\n                            undefined,\n                            message.textBody,\n                            undefined);\n\n                        if (!!this.privConversationServiceConnector.translationReceived) {\n                            this.privConversationServiceConnector.translationReceived(this.privConversationServiceConnector,\n                                new ConversationReceivedTranslationEventArgs(ConversationTranslatorMessageTypes.instantMessage, textResult, sessionId));\n                        }\n                        break;\n\n                    default:\n                        // ignore any unsupported message types\n                        break;\n                }\n            } catch (e) {\n                // continue\n            }\n            return this.receiveConversationMessageOverride();\n        } catch (e) {\n            this.terminateMessageLoop = true;\n        }\n\n        return communicationCustodian.promise;\n    }\n\n    private async startMessageLoop(): Promise<void> {\n        if (this.isDisposed()) {\n            return Promise.resolve();\n        }\n        this.terminateMessageLoop = false;\n\n        const messageRetrievalPromise = this.receiveConversationMessageOverride();\n\n        try {\n            const r = await messageRetrievalPromise;\n            return r;\n        } catch (error) {\n            this.cancelRecognition(this.privRequestSession ? this.privRequestSession.sessionId : \"\", this.privRequestSession ? this.privRequestSession.requestId : \"\", CancellationReason.Error, CancellationErrorCode.RuntimeError, error as string);\n            return null;\n        }\n    }\n\n    // Takes an established websocket connection to the endpoint\n    private configConnection(): Promise<IConnection> {\n        if (this.isDisposed()) {\n            return Promise.resolve<IConnection>(undefined);\n        }\n        if (this.privConnectionConfigPromise !== undefined) {\n            return this.privConnectionConfigPromise.then((connection: IConnection): Promise<IConnection> => {\n                if (connection.state() === ConnectionState.Disconnected) {\n                    this.privConnectionId = null;\n                    this.privConnectionConfigPromise = undefined;\n                    return this.configConnection();\n                }\n                return this.privConnectionConfigPromise;\n            }, (): Promise<IConnection> => {\n                this.privConnectionId = null;\n                this.privConnectionConfigPromise = undefined;\n                return this.configConnection();\n            });\n        }\n        if (this.terminateMessageLoop) {\n            return Promise.resolve<IConnection>(undefined);\n        }\n\n        this.privConnectionConfigPromise = this.connectImpl().then((connection: IConnection): IConnection => connection);\n\n        return this.privConnectionConfigPromise;\n    }\n\n    private getTranslations(serviceResultTranslations: ITranslationResponsePayload[]): Translations {\n        let translations: Translations;\n\n        if (undefined !== serviceResultTranslations) {\n            translations = new Translations();\n            for (const translation of serviceResultTranslations) {\n                translations.set(translation.lang, translation.translation);\n            }\n        }\n\n        return translations;\n    }\n}\n"]}