acs_webchat-chat-adapter
v0.0.35-beta.30.1
Published
An adapter for connecting WebChat with Azure Communication Services
Downloads
7,645
Readme
Introduction
ACS Webchat Adapter is a project for connecting Webchat UX to ACS chat.
Creating an adapter
createACSAdapter = (
token: string,
id: string,
threadId: string,
environmentUrl: string,
fileManager: IFileManager,
pollingInterval: number,
eventSubscriber: IErrorEventSubscriber,
displayName?: string,
chatClient?: ChatClient,
logger?: ILogger,
adapterOptions?: AdapterOptions
): SealedAdapter<IDirectLineActivity, ACSAdapterState>
token
An ACS user access tokenid
The ACS user's idthreadId
Id of the chat thread to joinenvironmentUrl
ACS resource endpointfileManager
IFileManager instance for filesharingpollingInterval
Interval in milliseconds that the adapter will poll for messages, polling is only done when notifications are not enabled. The minimum value is 5 seconds. Default value is 30 seconds.
eventSubscriber
IErrorEventSubscriber instance to send error events to the caller.
displayName
User's displaynamechatClient
Chat Clientlogger
Logger instanceadapterOptions
Adapter options, see Feature Options for more detail
Before you start:
ACS Webchat Adapter requires CommunicationUserToken
and EnvironmentUrl
. You can either directly use ConnectionString
of ACS service or can host token management service
project.
Option1: Use ConnectionString Directly
Get the connection string from ACS Communication Service In local.settings.json replace [CONNECTION_STRING] with connection string
{
"ConnectionString": "endpoint=https://********;accesskey=******"
}
Option2: Token Management Service Project
If the token management project is running, paste the service URL to:
In webpack.dev.config.js, replace hosted_function_url
with your own connection string
{
search: 'TOKEN_MANAGEMENT_SERVICE_URL',
replace: '' // URL of token management service
}
Getting Started
- Install nodejs
npm install
npm run start
- open localhost:8080 in browser
- copy&paste address bar to another tab to turn on another chat client
- 2 clients can now talk together!
Project structure
src
├── egress
│ └── createEgress[ActivityType]ActivityMiddleware.ts
├── ingress
│ └── subsribe[Event].ts
├── sdk
├── index.html
ingress
contains all the event subscribe middleware which listens to ACS event and dispatchs activity to Webchat
egress
contains all the middles which applied to WebChat and listen to different actions and call sdks
sdk
contains api wrappers for sdk including authentication
index.html
is a demo html running by npm run start
, which could be used as sample code
How it works
Install VSCode mermaid extension to view this diagram in VSCode
There are 2 token types:
- User Token is what the web app uses to validate a login user
- ACS Token is what ACS Adapter use to communicate with Azure Communication Service
Handshake process:
sequenceDiagram
participant ACS Token Service
participant App Web Server
participant Web App
participant WebChat
participant ACS Adapter
participant ACS Chat SDK
participant Azure Communication Service
Web App-->>App Web Server: Send User Token for authentication and ask for an ACS token
App Web Server -->> App Web Server: Validate the user using User Token
App Web Server -->> ACS Token Service: Create an ACS token
ACS Token Service -->> App Web Server: Return an ACS token
App Web Server -->> App Web Server: Bind User Identity with ACS token
App Web Server -->> Web App: Return an ACS token
Web App -->> ACS Adapter: Use ACS token to create adapter
Web App -->> WebChat: Render Webchat and bind with adapter
ACS Adapter -->> ACS Chat SDK: Create ChatClient using ACS token
ACS Chat SDK -->> Azure Communication Service: Network request to service
Notes:
ACS doesn't provide a login service, all authentications depends on the original app authentication, and App Server is responsible for assign an ACS token after user validation(then bind ACS token with user info)
Every time when ACS Token Service is asked for creating a new User Token, a new ACS user is created, token binding should happen again
Thread Creation logic could happen either in App Web Server or in Adapter, it is just a preference of whether developers want it to be a heavy server depending or light server depending app
Message sending and receiving:
sequenceDiagram
participant WebChat
participant ACS Adapter
participant ACS Chat SDK
participant Azure Communication Service
WebChat -->> WebChat: User sends messages
WebChat -->> ACS Adapter: Pass message as an Activity to ACS Adapter
ACS Adapter -->> ACS Chat SDK: Call chatClient.sendMessage(message)
ACS Chat SDK -->> Azure Communication Service: Send post message network request
Azure Communication Service -->> ACS Chat SDK: Receive message from network
ACS Chat SDK -->> ACS Adapter: Trigger receive message event
ACS Adapter -->> WebChat: Dispatch IncomingActivity
Details for waiting queue:
sequenceDiagram
participant ACS Adapter
participant ChatWidget
participant Edge Server
participant Azure Communication Service
Edge Server -->> Azure Communication Service: Join Conversation (using ACS Chat SDK)
Edge Server -->> Azure Communication Service: Create token and add customer into thread
Edge Server -->> ChatWidget: token & threadId
ChatWidget -->> ACS Adapter: token & threadId
Edge Server -->> Azure Communication Service: Send queue information (using chatClient.sendMessage)
Azure Communication Service -->> ACS Adapter: Dispatch queue messgae
ACS Adapter -->> ChatWidget: Dispatch Activity with ChannelData
Edge Server -->> Azure Communication Service: Agent ready, create token and join agent to thread
Edge Server -->> Azure Communication Service: Agent ready, create token and join the agent to the thread
Azure Communication Service -->> ACS Adapter: trigger threadUpdate
Details for idle status:
sequenceDiagram
participant ACS Adapter
participant ChatWidget
participant Edge Server
participant Azure Communication Service
ACS Adapter -->> Edge Server: Send heartbeat
ACS Adapter -->> Edge Server: Stop heartbeat
Edge Server -->> Edge Server: no heartbeat received for 90s
Edge Server -->> Azure Communication Service: kick user out of the thread
Azure Communication Service -->> ACS Adapter: Notify thread update
ACS Adapter -->> ChatWidget: User left the chat
Build and Test
Build a dev test js file
Dev Test js file can work without real server logic - check before you start in readme
for more details
- Run
npm run build:dev
- Get webchat-adapter-dev.js in dist file
- Add webchat-adapter-dev.js as
<script>
tag in html, and call window.ChatAdapter.initializeThread() - Call window.ChatAdapter.createACSAdapter (check sample code in index.html)
Build a consumable js file
- Run
npm run build
- Get webchat-adapter.js in dist file
- Get all the parameters for createACSAdapter from server side
- Add webchat-adapter.js as
<script>
tag in html, and call window.ChatAdapter.createACSAdapter (check sample code in index.html)
Telemetry
To use telemetry for adapter, you will need to implement Logger api for adapter
interface ILogger {
logEvent(loglevel: LogLevel, event: ACSLogData): void;
}
Error event notifier
To use the error event notifier, you will need to implement the following method
interface IErrorEventSubscriber {
notifyErrorEvent(adapterErrorEvent: AdapterErrorEvent): void;
}
And pass it when you call window.ChatAdapter.createACSAdapter (put it in the 6th parameter), check our demo index.html for reference
Feature Options
The following features can be enabled by injecting AdapterOptions
interface.
export interface AdapterOptions {
enableAdaptiveCards: boolean; // to enable adaptive card payload in adapter (which will convert content payload into a json string)
enableThreadMemberUpdateNotification: boolean; // to enable chat thread member join/leave notification
enableLeaveThreadOnWindowClosed: boolean; // to remove user on browser close event
enableSenderDisplayNameInTypingNotification?: boolean; // Whether to send sender display name in typing notification,
historyPageSizeLimit?: number; // If requested paged responses of messages otherwise undefined
serverPageSizeLimit?: number; // Number of messages to fetch from the server at once
shouldFileAttachmentDownloadTimeout?: boolean; // Whether file attachment download be timed out.
fileAttachmentDownloadTimeout?: number; // If shouldFileAttachmentDownloadTimeout is set then timeout value in milliseconds when attachment download should be timed out. Default value 90s.
messagePollingHandle?: IMessagePollingHandle; // Provides more control over polling calls made to Chat Gateway service.
}
Adapter options are injected to directline while creating ACS adapter.
const featuresOption = {
enableAdaptiveCards: true,
enableThreadMemberUpdateNotification: true,
enableLeaveThreadOnWindowClosed: true,
enableSenderDisplayNameInTypingNotification: true,
historyPageSizeLimit: 5,
serverPageSizeLimit: 60,
shouldFileAttachmentDownloadTimeout: true,
fileAttachmentDownloadTimeout: 120000
};
const directLine = createACSAdapter(token, userId, threadId, environmentUrl, fileManager, displayName, logger, featuresOption);
Option | Type | Default | Description |
--- | --- | --- | --- |
enableThreadMemberUpdateNotification | boolean | false | Send chat thread member update notification activities to WebChat when a new user joins thread or a user leave the thread. |
enableAdaptiveCards | boolean | false | The Adaptive Cards are sent as attachments in activity. The format is followd as per guidelines. Development Panel has adaptive card implementation.The example code can be found at location src/development/react-componets/.adaptiveCard.tsx |
enableLeaveThreadOnWindowClosed | boolean | false | On browser close, whether users will remove themselves from the chat thread. |
enableSenderDisplayNameInTypingNotification | boolean | false | Whether send user display name when sending typing indicator. |
historyPageSizeLimit | number | undefined | History message pagination can be turned on via setting a valid historyPageSizeLimit. To send a pagination event: window.dispatchEvent(new Event('acs-adapter-loadnextpage'));|
serverPageSizeLimit | number | undefined | Number of messages to fetch from the server at once. Putting a high value like 60 will result in a fewer calls to the server to fetch all the messages on a thread. |
shouldFileAttachmentDownloadTimeout | boolean | undefined | Whether file attachment download be timed out. |
fileAttachmentDownloadTimeout | number | undefined | If the shouldFileAttachmentDownloadTimeout
is set then the value of timeout when file download should be timed out. Default will be 90000ms enforced only when shouldFileAttachmentDownloadTimeout
is true, otherwise will wait for browser timeout. |
messagePollingHandle | Interface IMessagePollingHandle | undefined | Provides methods to control polling. |
IMessagePollingHandle Interface
IMessagePollingHandle
has been added to provide more control over polling calls that Adapter invokes to fetch messages in the background. In some cases clients might want to stop these calls from their end either completely or temporarily when a user is removed as a participant. Interface provides two methods whose description is below that client could implement and provide a reference to in AdapterOptions
.
Method | Description | Return value | Default |
--- | --- | --- | --- |
getIsPollingEnabled: () => boolean;
| Get client polling setting during each polling cycle. This only disables the call to the service but the scheduler is still on going | Return true
to execute message fetching call for current cycle. Return false
to skip message fetching call for current cycle. | true
|
stopPolling: () => boolean;
| This stops the scheduler from scheduling any further message fetching calls. | Return true
to stop scheduling any next message fetching calls. Return false
to continue scheduling next message fetching calls | false
|
Leave Chat Event
Instead of leaving the thread in Server side, we also allow user to leave thread using our adapter. To leave chat raise a 'acs-adapter-leavechat' event. The sample code is written in file 'src\development\react-component\leavechat.tsx'. Development Panel has leave chat button implemented.
window.dispatchEvent(new Event('acs-adapter-leavechat'));
Egress Error
When egress
ACS backend throws an exception, an activity with channel data of type error
will be triggered.
To display message to user, you can detect activity of type error like below. The message and stack of the Error
object is saved in the activity text property.
if (
action.payload &&
action.payload.activity?.channelData &&
action.payload.activity?.channelData.type == 'Error'
) {
dispatch({
type: 'WEB_CHAT/SET_NOTIFICATION',
payload: {
level: 'info',
message: message: JSON.parse(action.payload.activity.text).payload.details.Message
}
});
Development Panel
Development Panel is integrated inside of dev mode js file, if you import dev mode in html(Check index.html for Reference):
const {
renderDebugPanel
} = window.ChatAdapter;
/*
Create adapter and store...
*/
window.WebChat.renderWebChat(
{
directLine,
store,
},
document.getElementById('root')
);
renderDebugPanel(document.getElementById('devPannel'), store);
Development panel has Adaptive Card UI panel and Leave Chat button implementation.
To launch development panel in dev mode:
- Run
npm run start
- Browse http://localhost:8080/index.html
- To show/hide (toggle) development panel press Ctrl+d
Test and unit test debug
All the unit test files are under /test/unit folder, run npm run test:unit
to test
To run a single test file for instance, run npm run test:unit -- test/unit/ingress/subscribeNewMessageAndThreadUpdate.test.ts
When you are writing test and wanna debug it:
- Run
npm run test:debug
to test - Switch to VSCode debug tab and choose attach
- VSCode debug tool should be able to connect to debug thread, set breakpoints and have fun!
Integration Test
All the integration test files are under /test/integration folder
To test:
- Run
npm run install:chromedriver
to install chrome driver - Run
npm run execute:integrationtest
to test
Filesharing
To enable filesharing implement the IFileManager interface and provide an instance of this when creating the adapter. See index.html for an example using the OneDriveFileManager implementation.
activity.attachments
Contains the attachment data. Each attachment contains the following properties:
contentType
The MIME-typecontentUrl
The file contents URLname
The filenamethumbnailUrl
Applicable to images and videos only, the image thumbnail URL
When sending attachments ensure the attachments
property is populated on the Message activity that is sent to the adapter, this is done by Webchat component.
When receiving messages the adapter will populate this property if the ChatMessage contains attachments.
Note since filesharing is supported through metadata activity.channelData.metadata
will contain the property returned by IFileManager's createChatMessageMetadata()
method. You do NOT need to explicitly set this in the metadata yourself.
Example of an activity with attachments that Webchat component sends to adapter:
const activity: ACSDirectLineActivity = {
...
attachments: [
{
filename: "<YOUR_FILENAME>",
contentType: "<YOUR_FILETYPE>"
contentURL: "<YOUR_URL>"
thumbnailURL: "<YOUR_THUMBNAIL_URL>"
}
]
...
}
Note that when using Webchat components built-in file attachment capabilities, you should not need to construct this activity yourself.
attachments
are passed directly to IFileManager
uploadFiles() method, if you require additional data for file upload you can modify the attachments in the activity.
For example in the attachments below, myAdditionalData
will be passed inside attachments to uploadFile():
attachments: {
filename: "<YOUR_FILENAME>",
contentType: "<YOUR_FILETYPE>"
contentURL: "<YOUR_URL>"
thumbnailURL: "<YOUR_THUMBNAIL_URL>"
myAdditionalData: "<YOUR_ADDITIONAL_DATA>"
}
Tags
Adapter supports message tagging. Tags are sent as ChatMessage metadata.
activity.channelData.tags: string
To send tags to the adapter, populate this property inside the activity. When receiving activities from the adapter, this property will be populated if the ChatMessage contains tags.
Note since tags are sent as metadata activity.channelData.metadata.tags
will also contain the tags. You do NOT need to explicitly set this in the metadata yourself.
Exampe of sending tags in an Activity:
const activity: ACSDirectLineActivity = {
...
channelData: {
tags: "tag1"
}
...
}
And the resulting metadata on the ChatMessage object from ACS:
const message: ChatMessage = {
...
metadata: {
tags: "tag1"
}
...
}
Metadata
ChatMessages may contain metadata, which is a property bag of string key-value pairs.
Message tagging and filesharing are supported through ChatMessage metadata, you do NOT need to explicitly set these in the metadata yourself, the adapter will do that for you. You can however send additional metadata if needed.
You can send additional ChatMessage metadata by populating the activity.channelData.metadata
property which is of type Record<string, string>
. When receiving ChatMessages, the adapter will populate this property if the message contains metadata.
Example Activity showing metadata format:
const activity: ACSDirectLineActivity = {
...
channelData: {
metadata: {
"<YOUR_KEY>" : "<YOUR_VALUE>"
}
}
...
}
Message Edits
Note: Webchat component does not support message edits. Note: If adaptive cards are enabled, the adapter expects the message content to be a JSON object with text property. For example, valid content for an adaptive card:
JSON.stringify({
text: "Hello world!"
})
The adapter listens for ChatMessageEditedEvents and will dispatch an MessageEdit activity when incoming events are received.
Since message edits are unsupported by Webchat, MessageEdit Activities are custom activity types that you must handle yourself.
MessageEdit activities contain the edited content (which may be the message content or metadata) as well as the timestamp when the message was edited. The MessageEdit Activity will have the same messageid as the original ChatMessage.
Below shows the format of a MessageEdit Activity, see the ACSDirectLineActivity for more type information:
const activity: IDirectLineActivity = {
attachment?s: any[],
channelId: string,
channelData?: {
attachmentSizes: number[],
tags: string,
metadata: Record<string, string>;
},
conversation: { id: string },
from: {
id: string,
name: string,
role: string
},
messageid: string,
text: string,
timestamp: string,
type: ActivityType.Message
}
The following properties contain information about the message edit:
text
is the edited message's contenttimestamp
is the timestamp when the message was edited, in ISO 8601 formatmessageid
is the id of the edited messagetype
is ActivityType.Message which is equal to "message"
If the edited message contains metadata the following properties will be present:
attachments
is an array of objects containing file attachment data, see Filesharing for more detail.channelData.tags
contains the message tags (this is an optional metadata property)channelData.metadata
contains the message metadata
Adaptive Cards
Enable Adaptive Cards by setting enableAdaptiveCards
inside AdapterOptions
to true
. When enabled, the adapter will determine if the received message is an adaptive card & format the activity appropriately. If adaptive cards are not enabled then Webchat will render messages as is.
ACS Adapter expects the ChatMessage for an adaptive card to contain:
- The adaptive card JSON as the message content
- The ChatMessage metadata to contain the key
microsoft.azure.communication.chat.bot.contenttype
with a value ofazurebotservice.adaptivecard
Custom Middleware
NOTE Webchat currently supports activityMiddleware and createStore middleware, these can also be utilized for similar effect.
You can provide your own custom egress and ingress middlewares when initializing the adapter.
Inside AdapterOptions
, provde ingressMiddleware
and/or egressMiddleware
.
ingressMiddleware
: An array of middlewares that are invoked on incoming messages from ACS. The middlewares will be applied in the order they are provided. Ingress middleware can be used to intercept messages received from ACS.
egressMiddleware
: An array of middlewares that are invoked on outgoing messages to ACS. The middlewares will be applied in the order they are provided. Egress middleware can be used to intercept messages before sending them to ACS.
Example:
const myEgressMiddleware = ({ getState }) => (next) => (activity) => {
if (activity.type === ActivityType.Message) {
// Do something with the message before sending it to ACS
}
return next(activity);
}
const options: AdapterOptions = {
enableAdaptiveCards: false,
enableThreadMemberUpdateNotification: true,
enableLeaveThreadOnWindowClosed: true,
historyPageSizeLimit: 5,
egressMiddleware: [myEgressMiddleware]
};
const adapter = createACSAdapter(
token,
userId,
threadId,
environmentUrl,
fileManager,
pollingInterval,
displayName,
logger,
options
);
Contribute
TODO: Explain how other users and developers can contribute to make your code better.
If you want to learn more about creating good readme files then refer the following guidelines. You can also seek inspiration from the below readme files: