// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. import { createTrouterService, ITrouterServiceBase, ITrouterServiceConfig, TrouterState, StateChangedListener, UserActivityState, } from "@skype/tstrouter"; import { toMessageHandler, toLogProvider, toTelemetrySender } from "./TrouterUtils"; import { defaultTelemetrySettings, createSettings } from "./TrouterSettings"; import { ChatEventId, BaseChatEvent, BaseChatMessageEvent, ChatMessageReceivedEvent, ChatMessageEditedEvent, ChatMessageDeletedEvent, ReadReceiptReceivedEvent, TypingIndicatorReceivedEvent, BaseChatThreadEvent, ChatParticipant, ChatAttachment, ChatAttachmentType, ChatThreadProperties, ChatThreadCreatedEvent, ChatThreadDeletedEvent, ChatThreadPropertiesUpdatedEvent, ParticipantsAddedEvent, ParticipantsRemovedEvent, ChatRetentionPolicy, NoneRetentionPolicy, ThreadCreationDateRetentionPolicy, } from "./events/chat"; import { CommunicationIdentifier, CommunicationUserIdentifier, PhoneNumberIdentifier, MicrosoftTeamsUserIdentifier, TeamsExtensionUserIdentifier, UnknownIdentifier, CommunicationIdentifierKind, CommunicationUserKind, PhoneNumberKind, MicrosoftTeamsUserKind, TeamsExtensionUserKind, MicrosoftTeamsAppKind, MicrosoftTeamsAppIdentifier, UnknownIdentifierKind, } from "./events/identifierModels"; import { MAX_NUMBER_OF_TOKEN_FETCH_RETRIES } from "./constants"; import { AzureLogger } from "@azure/logger"; import { AbortSignalLike } from "@azure/abort-controller"; import { AccessToken } from "@azure/core-auth"; import { AdditionalPolicyConfig } from "@azure/core-client"; import { UserAgentPolicyOptions } from "@azure/core-rest-pipeline"; export enum ConnectionState { Unknown = 0, Connected = 2, Disconnected = 3, Switching = 9, } export interface SignalingClientOptions { registrationTimeInMs?: number; resourceEndpoint?: string; gatewayApiVersion?: string; additionalPolicies?: AdditionalPolicyConfig[]; userAgentOptions?: UserAgentPolicyOptions; } export { ChatEventId, BaseChatEvent, BaseChatMessageEvent, ChatAttachment, ChatAttachmentType, ChatMessageReceivedEvent, ChatMessageEditedEvent, ChatMessageDeletedEvent, ReadReceiptReceivedEvent, TypingIndicatorReceivedEvent, BaseChatThreadEvent, ChatParticipant, ChatThreadProperties, ChatThreadCreatedEvent, ChatThreadDeletedEvent, ChatThreadPropertiesUpdatedEvent, ParticipantsAddedEvent, ParticipantsRemovedEvent, CommunicationIdentifier, CommunicationUserIdentifier, PhoneNumberIdentifier, MicrosoftTeamsUserIdentifier, TeamsExtensionUserIdentifier, UnknownIdentifier, CommunicationIdentifierKind, CommunicationUserKind, PhoneNumberKind, MicrosoftTeamsUserKind, TeamsExtensionUserKind, MicrosoftTeamsAppKind, MicrosoftTeamsAppIdentifier, UnknownIdentifierKind, ChatRetentionPolicy, NoneRetentionPolicy, ThreadCreationDateRetentionPolicy, }; /** * Options for `CommunicationTokenCredential`'s `getToken` function. */ export interface CommunicationGetTokenOptions { /** * An implementation of `AbortSignalLike` to cancel the operation. */ abortSignal?: AbortSignalLike; } /** * The Azure Communication Services token credential. */ export interface CommunicationTokenCredential { /** * Gets an `AccessToken` for the user. Throws if already disposed. * @param options - Additional options. */ getToken(options?: CommunicationGetTokenOptions): Promise; } export interface SignalingClient { /** * Start the realtime connection. */ start(): void; /** * Stop the realtime connection and unsubscribe all event handlers. */ stop(isTokenExpired?: boolean): void; /** * Listen to connectionChanged events. */ on(event: "connectionChanged", listener: (state: ConnectionState) => void): void; /** * Listen to chatMessageReceived events. */ on(event: "chatMessageReceived", listener: (payload: ChatMessageReceivedEvent) => void): void; /** * Listen to typingIndicatorReceived events. */ on( event: "typingIndicatorReceived", listener: (payload: TypingIndicatorReceivedEvent) => void ): void; /** * Listen to readReceiptReceived events. */ on(event: "readReceiptReceived", listener: (payload: ReadReceiptReceivedEvent) => void): void; /** * Listen to chatMessageEdited events. */ on(event: "chatMessageEdited", listener: (payload: ChatMessageEditedEvent) => void): void; /** * Listen to chatMessageDeleted events. */ on(event: "chatMessageDeleted", listener: (payload: ChatMessageDeletedEvent) => void): void; /** * Listen to chatThreadCreated events. */ on(event: "chatThreadCreated", listener: (payload: ChatThreadCreatedEvent) => void): void; /** * Listen to chatThreadPropertiesUpdated events. */ on( event: "chatThreadPropertiesUpdated", listener: (payload: ChatThreadPropertiesUpdatedEvent) => void ): void; /** * Listen to chatThreadDeleted events. */ on(event: "chatThreadDeleted", listener: (payload: ChatThreadDeletedEvent) => void): void; /** * Listen to participantsAdded events. */ on(event: "participantsAdded", listener: (payload: ParticipantsAddedEvent) => void): void; /** * Listen to participantsRemoved events. */ on(event: "participantsRemoved", listener: (payload: ParticipantsRemovedEvent) => void): void; } export class CommunicationSignalingClient implements SignalingClient { private readonly trouter: ITrouterServiceBase; private config: ITrouterServiceConfig; private stateChangedListener: StateChangedListener = null; private tokenFetchRetries: number = 0; private resourceEndpoint: string; private gatewayApiVersion: string; constructor( private readonly credential: CommunicationTokenCredential, private readonly logger: AzureLogger, private readonly options?: SignalingClientOptions ) { this.trouter = createTrouterService(toLogProvider(logger)); } public async start(): Promise { this.resourceEndpoint = this.options?.resourceEndpoint; if (this.resourceEndpoint === undefined) { throw new Error("'endpoint' cannot be null"); } this.gatewayApiVersion = this.options?.gatewayApiVersion || "2024-03-07"; if (this.config === undefined) { this.config = { trouterSettings: await createSettings(this.credential, this.options), skypeTokenProvider: async (forceRefresh: boolean) => { if (forceRefresh) { this.tokenFetchRetries += 1; if (this.tokenFetchRetries > MAX_NUMBER_OF_TOKEN_FETCH_RETRIES) { await this.stop(true); throw new Error( `Access token is expired and failed to fetch a valid one after ${MAX_NUMBER_OF_TOKEN_FETCH_RETRIES} retries` ); } } else { this.tokenFetchRetries = 0; } return Promise.resolve((await this.credential.getToken()).token); }, telemetryConfig: { eventLogger: toTelemetrySender(this.logger), settings: defaultTelemetrySettings, }, }; } this.trouter.start(this.config); this.trouter.setUserActivityState(UserActivityState.Active); } public async stop(isTokenExpired?: boolean): Promise { this.trouter.offStateChanged(this.stateChangedListener); this.trouter.clearMessageHandlers(); this.trouter.stop(isTokenExpired ?? this.tokenFetchRetries > MAX_NUMBER_OF_TOKEN_FETCH_RETRIES); } public on(event: "connectionChanged", listener: (state: ConnectionState) => void): void; public on( event: "chatMessageReceived", listener: (payload: ChatMessageReceivedEvent) => void ): void; public on( event: "typingIndicatorReceived", listener: (payload: TypingIndicatorReceivedEvent) => void ): void; public on( event: "readReceiptReceived", listener: (payload: ReadReceiptReceivedEvent) => void ): void; public on(event: "chatMessageEdited", listener: (payload: ChatMessageEditedEvent) => void): void; public on( event: "chatMessageDeleted", listener: (payload: ChatMessageDeletedEvent) => void ): void; public on(event: "chatThreadCreated", listener: (payload: ChatThreadCreatedEvent) => void): void; public on( event: "chatThreadPropertiesUpdated", listener: (payload: ChatThreadPropertiesUpdatedEvent) => void ): void; public on(event: "chatThreadDeleted", listener: (payload: ChatThreadDeletedEvent) => void): void; public on(event: "participantsAdded", listener: (payload: ParticipantsAddedEvent) => void): void; public on( event: "participantsRemoved", listener: (payload: ParticipantsRemovedEvent) => void ): void; public on( event: ChatEventId | "connectionChanged", listener: (genericPayload: any) => void ): void { if (event === "connectionChanged") { this.trouter.offStateChanged(this.stateChangedListener); this.stateChangedListener = (state: TrouterState, _url: string) => listener(state); this.trouter.onStateChanged(this.stateChangedListener); return; } this.trouter.registerMessageHandler( toMessageHandler(event, listener, this.resourceEndpoint, this.gatewayApiVersion) ); } }