# 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 token  
**`id`** The ACS user's id  
**`threadId`** Id of the chat thread to join  
**`environmentUrl`** ACS resource endpoint  
**`fileManager`** IFileManager instance for filesharing  
**`pollingInterval`** 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 displayname  
**`chatClient`** Chat Client  
**`logger`** Logger instance  
**`adapterOptions`** Adapter options, see [Feature Options](#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](https://skype.visualstudio.com/SCC/_git/internal_acs_token-management?path=%2F&version=GBinitial&_a=contents) 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

1. Install nodejs
2. `npm install`
3. `npm run start`
4. open localhost:8080 in browser
5. copy&paste address bar to another tab to turn on another chat client
6. 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:

1. User Token is what the web app uses to validate a login user
2. ACS Token is what ACS Adapter use to communicate with Azure Communication Service

## Handshake process:

```mermaid
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:

1. 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)
2. 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

3. 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:

```mermaid
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:
```mermaid
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:
```mermaid
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

1. Run `npm run build:dev`
2. Get webchat-adapter-dev.js in dist file
3. Add webchat-adapter-dev.js as `<script>` tag in html, and call window.ChatAdapter.initializeThread()
4. Call window.ChatAdapter.createACSAdapter (check sample code in index.html)

## Build a consumable js file

1. Run `npm run build`
2. Get webchat-adapter.js in dist file
3. Get all the parameters for createACSAdapter from server side
4. 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](https://docs.microsoft.com/en-us/adaptive-cards/getting-started/bots). 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:

1. Run `npm run start`
2. Browse http://localhost:8080/index.html
3. 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:

1. Run `npm run test:debug` to test
2. Switch to VSCode debug tab and choose attach
3. 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:

1. Run `npm run install:chromedriver` to install chrome driver
2. 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-type  
`contentUrl` The file contents URL  
`name` The filename  
`thumbnailUrl` 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 content  
**`timestamp`** is the timestamp when the message was edited, in ISO 8601 format  
**`messageid`** is the id of the edited message  
**`type`** 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](#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:
1. The adaptive card JSON as the message content
2. The ChatMessage metadata to contain the key `microsoft.azure.communication.chat.bot.contenttype` with a value of `azurebotservice.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](https://docs.microsoft.com/en-us/azure/devops/repos/git/create-a-readme?view=azure-devops). You can also seek inspiration from the below readme files:

- [ASP.NET Core](https://github.com/aspnet/Home)
- [Visual Studio Code](https://github.com/Microsoft/vscode)
