diff --git a/directline-3.0-local.json b/directline-3.0-local.json deleted file mode 100644 index fef3049d..00000000 --- a/directline-3.0-local.json +++ /dev/null @@ -1,1248 +0,0 @@ -{ - "openapi": "3.0.0", - "info": { - "version": "v3", - "title": "Bot Connector - Direct Line API - v3.0", - "description": "Direct Line 3.0\r\n===============\r\n\r\n\r\nThe Direct Line API is a simple REST API for connecting directly to a single bot. This API is intended for developers\r\nwriting their own client applications, web chat controls, mobile apps, or service-to-service applications that will\r\ntalk to their bot.\r\n\r\nWithin the Direct Line API, you will find:\r\n\r\n* An **authentication mechanism** using standard secret/token patterns\r\n* The ability to **send** messages from your client to your bot via an HTTP POST message\r\n* The ability to **receive** messages by **WebSocket** stream, if you choose\r\n* The ability to **receive** messages by **polling HTTP GET**, if you choose\r\n* A stable **schema**, even if your bot changes its protocol version\r\n\r\nDirect Line 1.1 and 3.0 are both available and supported. This document describes Direct Line 3.0. For information\r\non Direct Line 1.1, visit the [Direct Line 1.1 reference documentation](/en-us/restapi/directline/).\r\n\r\n# Authentication: Secrets and Tokens\r\n\r\nDirect Line allows you to authenticate all calls with either a secret (retrieved from the Direct Line channel\r\nconfiguration page) or a token (which you may get at runtime by converting your secret).\r\n\r\nA Direct Line **secret** is a master key that can access any conversation, and create tokens. Secrets do not expire.\r\n\r\nA Direct Line **token** is a key for a single conversation. It expires but can be refreshed.\r\n\r\nIf you're writing a service-to-service application, using the secret may be simplest. If you're writing an application\r\nwhere the client runs in a web browser or mobile app, you may want to exchange your secret for a token, which only\r\nworks for a single conversation and will expire unless refreshed. You choose which security model works best for you.\r\n\r\nYour secret or token is communicated in the ```Authorization``` header of every call, with the Bearer scheme.\r\nExample below.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations/abc123/activities HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\n[other HTTP headers, omitted]\r\n```\r\n\r\nYou may notice that your Direct Line client credentials are different from your bot's credentials. This is\r\nintentional, and it allows you to revise your keys independently and lets you share client tokens without\r\ndisclosing your bot's password. \r\n\r\n## Exchanging a secret for a token\r\n\r\nThis operation is optional. Use this step if you want to prevent clients from accessing conversations they aren't\r\nparticipating in.\r\n\r\nTo exchange a secret for a token, POST to /v3/directline/tokens/generate with your secret in the auth header\r\nand no HTTP body.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/tokens/generate HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\n[other headers]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"conversationId\": \"abc123\",\r\n \"token\": \"RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xn\",\r\n \"expires_in\": 1800\r\n}\r\n```\r\n\r\nIf successful, the response is a token suitable for one conversation. The token expires in the seconds\r\nindicated in the ```expires_in``` field (30 minutes in the example above) and must be refreshed before then to\r\nremain useful.\r\n\r\nThis call is similar to ```/v3/directline/conversations```. The difference is that the call to\r\n```/v3/directline/tokens/generate``` does not start the conversation, does not contact the bot, and does not\r\ncreate a streaming WebSocket URL.\r\n* Call ```/v3/directline/conversations``` if you will distribute the token to client(s) and want them to \r\n initiate the conversation.\r\n* Call ```/v3/directline/conversations``` if you intend to start the conversation immediately.\r\n\r\n\r\n## Refreshing a token\r\n\r\nA token may be refreshed an unlimited number of times unless it is expired.\r\n\r\nTo refresh a token, POST to /v3/directline/tokens/refresh. This method is valid only for unexpired tokens.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/tokens/refresh HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xn\r\n[other headers]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"conversationId\": \"abc123\",\r\n \"token\": \"RCurR_XV9ZA.cwA.BKA.y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xniaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\",\r\n \"expires_in\": 1800\r\n}\r\n```\r\n\t\r\n\r\n# REST calls for a Direct Line conversation\r\n\r\nDirect Line conversations are explicitly opened by clients and may run as long as the bot and client participate\r\n(and have valid credentials). While the conversation is open, the bot and client may both send messages. More than\r\none client may connect to a given conversation and each client may participate on behalf of multiple users.\r\n\r\n## Starting a conversation\r\n\r\nClients begin by explicitly starting a conversation. If successful, the Direct Line service replies with a\r\nJSON object containing a conversation ID, a token, and a WebSocket URL that may be used later.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xn\r\n[other headers]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 201 Created\r\n[other headers]\r\n\r\n{\r\n \"conversationId\": \"abc123\",\r\n \"token\": \"RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xn\",\r\n \"expires_in\": 1800,\r\n \"streamUrl\": \"https://directline.botframework.com/v3/directline/conversations/abc123/stream?t=RCurR_XV9ZA.cwA...\"\r\n}\r\n```\r\n\r\nIf the conversation was started, an HTTP 201 status code is returned. HTTP 201 is the code that clients\r\nwill receive under most circumstances, as the typical use case is for a client to start a new conversation.\r\nUnder certain conditions -- specifically, when the client has a token scoped to a single conversation AND\r\nwhen that conversation was started with a prior call to this URL -- this method will return HTTP 200 to signify\r\nthe request was acceptable but that no conversation was created (as it already existed).\r\n\r\nYou have 60 seconds to connect to the WebSocket URL. If the connection cannot be established during this time,\r\nuse the reconnect method below to generate a new stream URL.\r\n\r\nThis call is similar to ```/v3/directline/tokens/generate```. The difference is that the call to\r\n```/v3/directline/conversations``` starts the conversation, contacts the bot, and creates a streaming WebSocket\r\nURL, none of which occur when generating a token.\r\n* Call ```/v3/directline/conversations``` if you will distribute the token to client(s) and want them to\r\n initiate the conversation.\r\n* Call ```/v3/directline/conversations``` if you intend to start the conversation immediately.\r\n\r\n## Reconnecting to a conversation\r\n\r\nIf a client is using the WebSocket interface to receive messages but loses its connection, it may need to reconnect.\r\nReconnecting requires generating a new WebSocket stream URL, and this can be accomplished by sending a GET request\r\nto the ```/v3/directline/conversations/{id}``` endpoint.\r\n\r\nThe ```watermark``` parameter is optional. If supplied, the conversation replays from the watermark,\r\nguaranteeing no messages are lost. If ```watermark``` is omitted, only messages received after the reconnection\r\ncall (```GET /v3/directline/conversations/abc123```) are replayed.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nGET /v3/directline/conversations/abc123?watermark=0000a-42 HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xn\r\n[other headers]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"conversationId\": \"abc123\",\r\n \"token\": \"RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0y8qbOF5xPGfiCpg4Fv0y8qqbOF5x8qbOF5xn\",\r\n \"streamUrl\": \"https://directline.botframework.com/v3/directline/conversations/abc123/stream?watermark=000a-4&t=RCurR_XV9ZA.cwA...\"\r\n}\r\n```\r\n\r\nYou have 60 seconds to connect to the WebSocket stream URL. If the connection cannot be established during this\r\ntime, issue another reconnect request to get an updated stream URL.\r\n\r\n## Sending an Activity to the bot\r\n\r\nUsing the Direct Line 3.0 protocol, clients and bots may exchange many different Bot Framework v3 Activites,\r\nincluding Message Activities, Typing Activities, and custom activities that the bot supports.\r\n\r\nTo send any one of these activities to the bot,\r\n\r\n1. the client formulates the Activity according to the Activity schema (see below)\r\n2. the client issues a POST message to ```/v3/directline/conversations/{id}/activities```\r\n3. the service returns when the activity was delivered to the bot, with an HTTP status code reflecting the\r\n bot's status code. If the POST was successful, the service returns a JSON payload containing the ID of the\r\n Activity that was sent.\r\n\r\nExample follows.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations/abc123/activities HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\n[other headers]\r\n\r\n{\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n },\r\n \"text\": \"hello\"\r\n}\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"id\": \"0001\"\r\n}\r\n```\r\n\r\nThe client's Activity is available in the message retrieval path (either polling GET or WebSocket) and is not\r\nreturned inline.\r\n\r\nThe total time to POST a message to a Direct Line conversation is:\r\n\r\n* Transit time to the Direct Line service,\r\n* Internal processing time within Direct Line (typically less than 120ms)\r\n* Transit time to the bot\r\n* Processing time within the bot\r\n* Transit time for HTTP responses to travel back to the client.\r\n\r\nIf the bot generates an error, that error will trigger an HTTP 502 error (\"Bad Gateway\") in\r\nthe ```POST /v3/directline/conversations/{id}/activities``` call.\r\n\r\n### Sending one or more attachments by URL\r\n\r\nClients may optionally send attachments, such as images or documents. If the client already has a URL for the\r\nattachment, the simplest way to send it is to include the URL in the ```contentUrl``` field of an Activity\r\nattachment object. This applies to HTTP, HTTPS, and ```data:``` URIs.\r\n\r\n### Sending a single attachment by upload\r\n\r\nOften, clients have an image or document on a device but no URL that can be included in the activity.\r\n\r\nTo upload an attachment, POST a single attachment to\r\nthe ```/v3/directline/conversations/{conversationId}/upload``` endpoint. The ```Content-Type```\r\nand ```Content-Disposition``` headers control the attachment's type and filename, respectively.\r\n\r\nA user ID is required. Supply the ID of the user sending the attachment as a ```userId``` parameter in the URL.\r\n\r\nIf uploading a single attachment, a message activity is sent to the bot when the upload completes.\r\n\r\nOn completion, the service returns the ID of the activity that was sent.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations/abc123/upload?userId=user1 HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\nContent-Type: image/jpeg\r\nContent-Disposition: name=\"file\"; filename=\"badjokeeel.jpg\"\r\n[other headers]\r\n\r\n[JPEG content]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"id\": \"0003\"\r\n}\r\n```\r\n\r\n### Sending multiple attachments by upload\r\n\r\nIf uploading multiple attachments, use ```multipart/form-data``` as the content type and include each\r\nattachment as a separate part. Each attachment's type and filename may be included in the ```Content-Type```\r\nand ```Content-Disposition``` headers in each part.\r\n\r\nAn activity may be included by adding a part with content type of ```application/vnd.microsoft.activity```.\r\nOther parts in the payload are attached to this activity before it is sent. If an Activity is not included,\r\nan empty Activity is created as a wrapper for the attachments.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations/abc123/upload?userId=user1 HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\nContent-Type: multipart/form-data; boundary=----DD4E5147-E865-4652-B662-F223701A8A89\r\n[other headers]\r\n\r\n----DD4E5147-E865-4652-B662-F223701A8A89\r\nContent-Type: image/jpeg\r\nContent-Disposition: form-data; name=\"file\"; filename=\"badjokeeel.jpg\"\r\n[other headers]\r\n\r\n[JPEG content]\r\n\r\n----DD4E5147-E865-4652-B662-F223701A8A89\r\nContent-Type: application/vnd.microsoft.activity\r\n[other headers]\r\n\r\n{\r\n \"type\": \"message\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n },\r\n \"text\": \"Hey I just IM'd you\\n\\nand this is crazy\\n\\nbut here's my webhook\\n\\nso POST me maybe\"\r\n}\r\n\r\n----DD4E5147-E865-4652-B662-F223701A8A89\r\n\r\n\r\n \r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"id\": \"0004\"\r\n}\r\n```\r\n\r\n## Receiving Activities from the bot\r\n\r\nDirect Line 3.0 clients may choose from two different mechanisms for retrieving messages:\r\n\r\n1. A **streaming WebSocket**, which pushes messages efficiently to clients.\r\n2. A **polling GET** interface, which is available for clients unable to use WebSockets or for clients\r\n retrieving the conversation history.\r\n\r\n**Not all activities are available via the polling GET interface.** A table of activity availability follows.\r\n\r\n|Activity type|Availability|\r\n|-------------|--------|\r\n|Message|Polling GET and WebSocket|\r\n|Typing|WebSocket only|\r\n|ConversationUpdate|Not sent/received via client|\r\n|ContactRelationUpdate|Not supported in Direct Line|\r\n|EndOfConversation|Polling GET and WebSocket|\r\n|All other activity types|Polling GET and WebSocket|\r\n\r\n### Receiving Activities by WebSocket\r\n\r\nTo connect via WebSocket, a client uses the StreamUrl when starting a conversation. The stream URL is\r\npreauthorized and does NOT require an Authorization header containing the client's secret or token.\r\n\r\n```\r\n-- connect to wss://directline.botframework.com --\r\nGET /v3/directline/conversations/abc123/stream?t=RCurR_XV9ZA.cwA...\" HTTP/1.1\r\nUpgrade: websocket\r\nConnection: upgrade\r\n[other headers]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 101 Switching Protocols\r\n[other headers]\r\n```\r\n\r\nThe Direct Line service sends the following messages:\r\n\r\n* An **ActivitySet**, which contains one or more activities and a watermark (described below)\r\n* An empty message, which the Direct Line service uses to ensure the connection is still valid\r\n* Additional types, to be defined later. These types are identified by the properties in the JSON root.\r\n\r\nActivitySets contain messages sent by the bot and by all users. Example ActivitySet:\r\n\r\n```\r\n{\r\n \"activities\": [{\r\n \"type\": \"message\",\r\n \"channelId\": \"directline\",\r\n \"conversation\": {\r\n \"id\": \"abc123\"\r\n },\r\n \"id\": \"abc123|0000\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n },\r\n \"text\": \"hello\"\r\n }],\r\n \"watermark\": \"0000a-42\"\r\n}\r\n```\r\n\r\nClients should keep track of the \"watermark\" value from each ActivitySet so they can use it on reconnect.\r\n**Note** that a ```null``` or missing watermark should be ignored and should not overwrite a prior watermark\r\nin the client.\r\n\r\nClients should ignore empty messages.\r\n\r\nClients may send their own empty messages to verify connectivity. The Direct Line service will ignore these.\r\n\r\nThe service may forcibly close the connection under certain conditions. If the client has not received an\r\nEndOfConversation activity, it may reconnect by issuing a GET request to the conversation endpoint to get a\r\nnew stream URL (see above).\r\n\r\nThe WebSocket stream contains live updates and very recent messages (since the call to get the WebSocket call\r\nwas issued) but it does not include messages sent prior to the most recent POST\r\nto ```/v3/directline/conversations/{id}```. To retrieve messages sent earlier in the conversation, use the\r\nGET mechanism below.\r\n\r\n### Receiving Activities by GET\r\n\r\nThe GET mechanism is useful for clients who are unable to use the WebSocket, or for clients wishing to retrieve\r\nthe conversation history.\r\n\r\nTo retrieve messages, issue a GET call to the conversation endpoint. Optionally supply a watermark, indicating\r\nthe most recent message seen. The watermark field accompanies all GET/WebSocket messages as a property in the\r\nActivitySet.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nGET /v3/directline/conversations/abc123/activities?watermark=0001a-94 HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\n[other headers]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"activities\": [{\r\n \"type\": \"message\",\r\n \"channelId\": \"directline\",\r\n \"conversation\": {\r\n \"id\": \"abc123\"\r\n },\r\n \"id\": \"abc123|0000\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n },\r\n \"text\": \"hello\"\r\n }, {\r\n \"type\": \"message\",\r\n \"channelId\": \"directline\",\r\n \"conversation\": {\r\n \"id\": \"abc123\"\r\n },\r\n \"id\": \"abc123|0001\",\r\n \"from\": {\r\n \"id\": \"bot1\"\r\n },\r\n \"text\": \"Nice to see you, user1!\"\r\n }],\r\n \"watermark\": \"0001a-95\"\r\n}\r\n```\r\n\r\nClients should page through the available activities by advancing the ```watermark``` value until no activities\r\nare returned.\r\n\r\n\r\n### Timing considerations \r\n\r\nMost clients wish to retain a complete message history. Even though Direct Line is a multi-part protocol with\r\npotential timing gaps, the protocol and service is designed to make it easy to build a reliable client.\r\n\r\n1. The ```watermark``` field sent in the WebSocket stream and GET response is reliable. You will not miss\r\n messages as long as you replay the watermark verbatim.\r\n2. When starting a conversation and connecting to the WebSocket stream, any Activities sent after the POST but\r\n before the socket is opened are replayed before new messages.\r\n3. When refreshing history by GET call while connected to the WebSocket, Activities may be duplicated across both\r\n channels. Keeping a list of all known Activity IDs will allow you to reject duplicate messages should they occur.\r\n\r\nClients using the polling GET interface should choose a polling interval that matches their intended use.\r\n\r\n* Service-to-service applications often use a polling interval of 5s or 10s.\r\n* Client-facing applications often use a polling interval of 1s, and fire an additional request ~300ms after\r\n every message the client sends to rapidly pick up a bot's response. This 300ms delay should be adjusted\r\n based on the bot's speed and transit time.\r\n\r\n## Ending a conversation\r\n\r\nEither a client or a bot may signal the end of a DirectLine conversation. This operation halts communication\r\nand prevents the bot and the client from sending messages. Messages may still be retrieved via the GET mechanism.\r\nSending this messages is as simple as POSTing an EndOfConversation activity.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations/abc123/activities HTTP/1.1\r\nAuthorization: Bearer RCurR_XV9ZA.cwA.BKA.iaJrC8xpy8qbOF5xnR2vtCX7CZj0LdjAPGfiCpg4Fv0\r\n[other headers]\r\n\r\n{\r\n \"type\": \"endOfConversation\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n }\r\n}\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 200 OK\r\n[other headers]\r\n\r\n{\r\n \"id\": \"0004\"\r\n}\r\n```\r\n\r\n## REST API errors\r\n\r\nHTTP calls to the Direct Line service follow standard HTTP error conventions:\r\n\r\n* 2xx status codes indicate success. (Direct Line 3.0 uses 200 and 201.)\r\n* 4xx status codes indicate an error in your request.\r\n * 401 indicates a missing or malformed Authorization header (or URL token, in calls where a token parameter\r\n is allowed).\r\n * 403 indicates an unauthorized client.\r\n * If calling with a valid but expired token, the ```code``` field is set to ```TokenExpired```.\r\n * 404 indicates a missing path, site, conversation, etc.\r\n* 5xx status codes indicate a service-side error.\r\n * 500 indicates an error inside the Direct Line service.\r\n * 502 indicates an error was returned by the bot. **This is a common error code.**\r\n* 101 is used in the WebSocket connection path, although this is likely handled by your WebSocket client.\r\n\r\nWhen an error message is returned, error detail may be present in a JSON response. Look for an ```error```\r\nproperty with ```code``` and ```message``` fields.\r\n\r\n```\r\n-- connect to directline.botframework.com --\r\nPOST /v3/directline/conversations/abc123/activities HTTP/1.1\r\n[detail omitted]\r\n\r\n-- response from directline.botframework.com --\r\nHTTP/1.1 502 Bad Gateway\r\n[other headers]\r\n\r\n{\r\n \"error\": {\r\n \"code\": \"BotRejectedActivity\",\r\n \"message\": \"Failed to send activity: bot returned an error\"\r\n }\r\n}\r\n```\r\n\r\nThe contents of the ```message``` field may change. The HTTP status code and values in the ```code```\r\nproperty are stable.\r\n\r\n# Schema\r\n\r\nThe Direct Line 3.0 schema is identical to the Bot Framework v3 schema.\r\n\r\nWhen a bot sends an Activity to a client through Direct Line:\r\n\r\n* attachment cards are preserved,\r\n* URLs for uploaded attachments are hidden with a private link, and\r\n* the ```channelData``` property is preserved without modification.\r\n\r\nWhen a client sends an Activity to a bot through Direct Line:\r\n\r\n* the ```type``` property contains the kind of activity you are sending (typically ```message```),\r\n* the ```from``` property must be populated with a user ID, chosen by your client,\r\n* attachments may contain URLs to existing resources or URLs uploaded through the Direct Line attachment\r\n endpoint, and\r\n* the ```channelData``` property is preserved without modification.\r\n\r\nClients and bots may send Activities of any type, including Message Activities, Typing Activities, and\r\ncustom Activity types.\r\n\r\nClients may send a single Activity at a time.\r\n\r\n```\r\n{\r\n \"type\": \"message\",\r\n \"channelId\": \"directline\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n },\r\n \"text\": \"hello\"\r\n}\r\n```\r\n\r\nClients receive multiple Activities as part of an ActivitySet. The ActivitySet has an array of activities\r\nand a watermark field.\r\n\r\n```\r\n{\r\n \"activities\": [{\r\n \"type\": \"message\",\r\n \"channelId\": \"directline\",\r\n \"conversation\": {\r\n \"id\": \"abc123\"\r\n },\r\n \"id\": \"abc123|0000\",\r\n \"from\": {\r\n \"id\": \"user1\"\r\n },\r\n \"text\": \"hello\"\r\n }],\r\n \"watermark\": \"0000a-42\"\r\n}\r\n```\r\n\r\n# Libraries for the Direct Line API\r\n\r\nThe Direct Line API is designed to be coded directly, but the Bot Framework includes libraries and controls that\r\nhelp you to embed Direct-Line-powered bots into your application.\r\n\r\n* The [Bot Framework Web Chat control](https://github.com/Microsoft/BotFramework-WebChat) is an easy way to embed\r\n the Direct Line protocol into a webpage.\r\n* [Direct Line Nuget package](https://www.nuget.org/packages/Microsoft.Bot.Connector.DirectLine) with libraries for\r\n .Net 4.5, UWP, and .Net Standard.\r\n* [DirectLineJs](https://github.com/Microsoft/BotFramework-DirectLineJs), also available on\r\n [NPM](https://www.npmjs.com/package/botframework-directlinejs)\r\n* You may generate your own from the [Direct Line Swagger file](swagger.json)\r\n\r\nOur [BotBuilder-Samples GitHub repo](https://github.com/Microsoft/BotBuilder-Samples) also contains samples for\r\n [C#](https://github.com/Microsoft/BotBuilder-Samples/tree/master/CSharp/core-DirectLine) and\r\n [JavaScript](https://github.com/Microsoft/BotBuilder-Samples/tree/master/Node/core-DirectLine).", - "termsOfService": "https://www.microsoft.com/en-us/legal/intellectualproperty/copyright/default.aspx", - "contact": { - "name": "Bot Framework", - "url": "https://botframework.com", - "email": "botframework@microsoft.com" - }, - "license": { - "name": "The MIT License (MIT)", - "url": "https://opensource.org/licenses/MIT" - } - }, - "servers": [{"url": "http://127.0.0.1:3978"}], - "schemes": [ - "https" - ], - "paths": { - "/v3/directline/conversations": { - "post": { - "tags": [ - "Conversations" - ], - "summary": "Start a new conversation", - "operationId": "Conversations_StartConversation", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml" - ], - "responses": { - "200": { - "description": "The conversation was successfully created, updated, or retrieved.", - "schema": { - "$ref": "#/definitions/Conversation" - } - }, - "201": { - "description": "The conversation was successfully created.", - "schema": { - "$ref": "#/definitions/Conversation" - } - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - }, - "409": { - "description": "The object you are trying to create already exists." - } - }, - "deprecated": false - } - }, - "/v3/directline/conversations/{conversationId}": { - "get": { - "tags": [ - "Conversations" - ], - "summary": "Get information about an existing conversation", - "operationId": "Conversations_ReconnectToConversation", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "conversationId", - "in": "path", - "required": true, - "type": "string" - }, - { - "name": "watermark", - "in": "query", - "required": false, - "type": "string" - } - ], - "responses": { - "200": { - "description": "The conversation was successfully created, updated, or retrieved.", - "schema": { - "$ref": "#/definitions/Conversation" - } - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - } - }, - "deprecated": false - } - }, - "/v3/directline/conversations/{conversationId}/activities": { - "get": { - "tags": [ - "Conversations" - ], - "summary": "Get activities in this conversation. This method is paged with the 'watermark' parameter.", - "operationId": "Conversations_GetActivities", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "conversationId", - "in": "path", - "description": "Conversation ID", - "required": true, - "type": "string" - }, - { - "name": "watermark", - "in": "query", - "description": "(Optional) only returns activities newer than this watermark", - "required": false, - "type": "string" - } - ], - "responses": { - "200": { - "description": "A set of activities and a watermark are returned.", - "schema": { - "$ref": "#/definitions/ActivitySet" - } - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - } - }, - "deprecated": false - }, - "post": { - "tags": [ - "Conversations" - ], - "summary": "Send an activity", - "operationId": "Conversations_PostActivity", - "consumes": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml", - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json", - "text/json", - "text/html" - ], - "parameters": [ - { - "name": "conversationId", - "in": "path", - "description": "Conversation ID", - "required": true, - "type": "string" - }, - { - "name": "activity", - "in": "body", - "description": "Activity to send", - "required": true, - "schema": { - "$ref": "#/definitions/Activity" - } - } - ], - "responses": { - "200": { - "description": "The operation succeeded.", - "schema": { - "$ref": "#/definitions/ResourceResponse" - } - }, - "204": { - "description": "The operation succeeded. No content was returned." - }, - "400": { - "description": "The URL, body, or headers in the request are malformed or invalid." - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - }, - "500": { - "description": "An internal server error has occurred." - }, - "502": { - "description": "The bot is unavailable or returned an error." - } - }, - "deprecated": false - } - }, - "/v3/directline/conversations/{conversationId}/upload": { - "post": { - "tags": [ - "Conversations" - ], - "summary": "Upload file(s) and send as attachment(s)", - "operationId": "Conversations_Upload", - "consumes": [ - "multipart/form-data" - ], - "produces": [ - "application/json", - "text/json", - "text/html" - ], - "parameters": [ - { - "name": "conversationId", - "in": "path", - "required": true, - "type": "string" - }, - { - "name": "userId", - "in": "query", - "required": false, - "type": "string" - }, - { - "name": "file", - "in": "formData", - "required": true, - "type": "file" - } - ], - "responses": { - "200": { - "description": "The operation succeeded.", - "schema": { - "$ref": "#/definitions/ResourceResponse" - } - }, - "202": { - "description": "The request was accepted for processing." - }, - "204": { - "description": "The operation succeeded. No content was returned." - }, - "400": { - "description": "The URL, body, or headers in the request are malformed or invalid." - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - }, - "500": { - "description": "An internal server error has occurred." - }, - "502": { - "description": "The bot is unavailable or returned an error." - } - }, - "deprecated": false - } - }, - "/v3/directline/tokens/refresh": { - "post": { - "tags": [ - "Tokens" - ], - "summary": "Refresh a token", - "operationId": "Tokens_RefreshToken", - "consumes": [], - "produces": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml" - ], - "responses": { - "200": { - "description": "The conversation was successfully created, updated, or retrieved.", - "schema": { - "$ref": "#/definitions/Conversation" - } - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - }, - "500": { - "description": "An internal server error has occurred." - } - }, - "deprecated": false - } - }, - "/v3/directline/tokens/generate": { - "post": { - "tags": [ - "Tokens" - ], - "summary": "Generate a token for a new conversation", - "operationId": "Tokens_GenerateTokenForNewConversation", - "consumes": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml", - "application/x-www-form-urlencoded" - ], - "produces": [ - "application/json", - "text/json", - "text/html", - "application/xml", - "text/xml" - ], - "parameters": [ - { - "name": "tokenParameters", - "in": "body", - "required": false, - "schema": { - "$ref": "#/definitions/TokenParameters" - } - } - ], - "responses": { - "200": { - "description": "The conversation was successfully created, updated, or retrieved.", - "schema": { - "$ref": "#/definitions/Conversation" - } - }, - "401": { - "description": "The operation included an invalid or missing Authorization header." - }, - "403": { - "description": "You are forbidden from performing this action because your token or secret is invalid." - }, - "404": { - "description": "The requested resource was not found." - }, - "500": { - "description": "An internal server error has occurred." - } - }, - "deprecated": false - } - } - }, - "definitions": { - "Conversation": { - "description": "An object representing a conversation or a conversation token", - "type": "object", - "properties": { - "conversationId": { - "description": "ID for this conversation", - "type": "string" - }, - "token": { - "description": "Token scoped to this conversation", - "type": "string" - }, - "expires_in": { - "format": "int32", - "description": "Expiration for token", - "type": "integer" - }, - "streamUrl": { - "description": "URL for this conversation's message stream", - "type": "string" - }, - "referenceGrammarId": { - "description": "ID for the reference grammar for this bot", - "type": "string" - }, - "eTag": { - "type": "string" - } - } - }, - "ActivitySet": { - "description": "A collection of activities", - "type": "object", - "properties": { - "activities": { - "description": "Activities", - "type": "array", - "items": { - "$ref": "#/definitions/Activity" - } - }, - "watermark": { - "description": "Maximum watermark of activities within this set", - "type": "string" - } - } - }, - "Activity": { - "description": "An Activity is the basic communication type for the Bot Framework 3.0 protocol", - "type": "object", - "properties": { - "type": { - "description": "The type of the activity [message|contactRelationUpdate|converationUpdate|typing|endOfConversation|event|invoke]", - "type": "string" - }, - "id": { - "description": "ID of this activity", - "type": "string" - }, - "timestamp": { - "format": "date-time", - "description": "UTC Time when message was sent (set by service)", - "type": "string" - }, - "localTimestamp": { - "format": "date-time", - "description": "Local time when message was sent (set by client, Ex: 2016-09-23T13:07:49.4714686-07:00)", - "type": "string" - }, - "serviceUrl": { - "description": "Service endpoint where operations concerning the activity may be performed", - "type": "string" - }, - "channelId": { - "description": "ID of the channel where the activity was sent", - "type": "string" - }, - "from": { - "$ref": "#/definitions/ChannelAccount", - "description": "Sender address" - }, - "conversation": { - "$ref": "#/definitions/ConversationAccount", - "description": "Conversation" - }, - "recipient": { - "$ref": "#/definitions/ChannelAccount", - "description": "(Outbound to bot only) Bot's address that received the message" - }, - "textFormat": { - "description": "Format of text fields [plain|markdown] Default:markdown", - "type": "string" - }, - "attachmentLayout": { - "description": "Hint for how to deal with multiple attachments: [list|carousel] Default:list", - "type": "string" - }, - "membersAdded": { - "description": "Array of address added", - "type": "array", - "items": { - "$ref": "#/definitions/ChannelAccount" - } - }, - "membersRemoved": { - "description": "Array of addresses removed", - "type": "array", - "items": { - "$ref": "#/definitions/ChannelAccount" - } - }, - "topicName": { - "description": "Conversations new topic name", - "type": "string" - }, - "historyDisclosed": { - "description": "True if the previous history of the channel is disclosed", - "type": "boolean" - }, - "locale": { - "description": "The language code of the Text field", - "type": "string" - }, - "text": { - "description": "Content for the message", - "type": "string" - }, - "speak": { - "description": "SSML Speak for TTS audio response", - "type": "string" - }, - "inputHint": { - "description": "Indicates whether the bot is accepting, expecting, or ignoring input", - "type": "string" - }, - "summary": { - "description": "Text to display if the channel cannot render cards", - "type": "string" - }, - "suggestedActions": { - "$ref": "#/definitions/SuggestedActions", - "description": "SuggestedActions are used to provide keyboard/quickreply like behavior in many clients" - }, - "attachments": { - "description": "Attachments", - "type": "array", - "items": { - "$ref": "#/definitions/Attachment" - } - }, - "entities": { - "description": "Collection of Entity objects, each of which contains metadata about this activity. Each Entity object is typed.", - "type": "array", - "items": { - "$ref": "#/definitions/Entity" - } - }, - "channelData": { - "$ref": "#/definitions/Object", - "description": "Channel-specific payload" - }, - "action": { - "description": "ContactAdded/Removed action", - "type": "string" - }, - "replyToId": { - "description": "The original ID this message is a response to", - "type": "string" - }, - "value": { - "$ref": "#/definitions/Object", - "description": "Open-ended value" - }, - "name": { - "description": "Name of the operation to invoke or the name of the event", - "type": "string" - }, - "relatesTo": { - "$ref": "#/definitions/ConversationReference", - "description": "Reference to another conversation or activity" - }, - "code": { - "description": "Code indicating why the conversation has ended", - "type": "string" - } - } - }, - "ChannelAccount": { - "description": "Channel account information needed to route a message", - "type": "object", - "properties": { - "id": { - "description": "Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456)", - "type": "string" - }, - "name": { - "description": "Display friendly name", - "type": "string" - } - } - }, - "ConversationAccount": { - "description": "Channel account information for a conversation", - "type": "object", - "properties": { - "isGroup": { - "description": "Is this a reference to a group", - "type": "boolean" - }, - "id": { - "description": "Channel id for the user or bot on this channel (Example: joe@smith.com, or @joesmith or 123456)", - "type": "string" - }, - "name": { - "description": "Display friendly name", - "type": "string" - } - } - }, - "SuggestedActions": { - "description": "SuggestedActions that can be performed", - "type": "object", - "properties": { - "to": { - "description": "Ids of the recipients that the actions should be shown to. These Ids are relative to the channelId and a subset of all recipients of the activity", - "type": "array", - "items": { - "type": "string" - } - }, - "actions": { - "description": "Actions that can be shown to the user", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - } - } - }, - "Attachment": { - "description": "An attachment within an activity", - "type": "object", - "properties": { - "contentType": { - "description": "mimetype/Contenttype for the file", - "type": "string" - }, - "contentUrl": { - "description": "Content Url", - "type": "string" - }, - "content": { - "$ref": "#/definitions/Object", - "description": "Embedded content" - }, - "name": { - "description": "(OPTIONAL) The name of the attachment", - "type": "string" - }, - "thumbnailUrl": { - "description": "(OPTIONAL) Thumbnail associated with attachment", - "type": "string" - } - } - }, - "Entity": { - "description": "Object of schema.org types", - "type": "object", - "properties": { - "type": { - "description": "Entity Type (typically from schema.org types)", - "type": "string" - } - } - }, - "Object": { - "type": "object", - "properties": {} - }, - "ConversationReference": { - "description": "An object relating to a particular point in a conversation", - "type": "object", - "properties": { - "activityId": { - "description": "(Optional) ID of the activity to refer to", - "type": "string" - }, - "user": { - "$ref": "#/definitions/ChannelAccount", - "description": "(Optional) User participating in this conversation" - }, - "bot": { - "$ref": "#/definitions/ChannelAccount", - "description": "Bot participating in this conversation" - }, - "conversation": { - "$ref": "#/definitions/ConversationAccount", - "description": "Conversation reference" - }, - "channelId": { - "description": "Channel ID", - "type": "string" - }, - "serviceUrl": { - "description": "Service endpoint where operations concerning the referenced conversation may be performed", - "type": "string" - } - } - }, - "CardAction": { - "description": "An action on a card", - "type": "object", - "properties": { - "type": { - "description": "Defines the type of action implemented by this button.", - "type": "string" - }, - "title": { - "description": "Text description which appear on the button.", - "type": "string" - }, - "image": { - "description": "URL Picture which will appear on the button, next to text label.", - "type": "string" - }, - "value": { - "$ref": "#/definitions/Object", - "description": "Supplementary parameter for action. Content of this property depends on the ActionType" - } - } - }, - "ResourceResponse": { - "description": "A response containing a resource ID", - "type": "object", - "properties": { - "id": { - "description": "Id of the resource", - "type": "string" - } - } - }, - "HeroCard": { - "description": "A Hero card (card with a single, large image)", - "type": "object", - "properties": { - "title": { - "description": "Title of the card", - "type": "string" - }, - "subtitle": { - "description": "Subtitle of the card", - "type": "string" - }, - "text": { - "description": "Text for the card", - "type": "string" - }, - "images": { - "description": "Array of images for the card", - "type": "array", - "items": { - "$ref": "#/definitions/CardImage" - } - }, - "buttons": { - "description": "Set of actions applicable to the current card", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - }, - "tap": { - "$ref": "#/definitions/CardAction", - "description": "This action will be activated when user taps on the card itself" - } - } - }, - "CardImage": { - "description": "An image on a card", - "type": "object", - "properties": { - "url": { - "description": "URL Thumbnail image for major content property.", - "type": "string" - }, - "alt": { - "description": "Image description intended for screen readers", - "type": "string" - }, - "tap": { - "$ref": "#/definitions/CardAction", - "description": "Action assigned to specific Attachment.E.g.navigate to specific URL or play/open media content" - } - } - }, - "ErrorResponse": { - "description": "An HTTP API response", - "type": "object", - "properties": { - "error": { - "$ref": "#/definitions/Error", - "description": "Error message" - } - } - }, - "Error": { - "description": "Object representing error information", - "type": "object", - "properties": { - "code": { - "description": "Error code", - "type": "string" - }, - "message": { - "description": "Error message", - "type": "string" - } - } - }, - "AnimationCard": { - "description": "An animation card (Ex: gif or short video clip)", - "type": "object", - "properties": { - "title": { - "description": "Title of the card", - "type": "string" - }, - "subtitle": { - "description": "Subtitle of the card", - "type": "string" - }, - "text": { - "description": "Text of the card", - "type": "string" - }, - "image": { - "$ref": "#/definitions/ThumbnailUrl", - "description": "Thumbnail placeholder" - }, - "media": { - "description": "Array of media Url objects", - "type": "array", - "items": { - "$ref": "#/definitions/MediaUrl" - } - }, - "buttons": { - "description": "Set of actions applicable to the current card", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - }, - "shareable": { - "description": "Is it OK for this content to be shareable with others (default:true)", - "type": "boolean" - }, - "autoloop": { - "description": "Should the client loop playback at end of content (default:true)", - "type": "boolean" - }, - "autostart": { - "description": "Should the client automatically start playback of video in this card (default:true)", - "type": "boolean" - } - } - }, - "ThumbnailUrl": { - "description": "Object describing a media thumbnail", - "type": "object", - "properties": { - "url": { - "description": "url pointing to an thumbnail to use for media content", - "type": "string" - }, - "alt": { - "description": "Alt text to display for screen readers on the thumbnail image", - "type": "string" - } - } - }, - "MediaUrl": { - "description": "MediaUrl data", - "type": "object", - "properties": { - "url": { - "description": "Url for the media", - "type": "string" - }, - "profile": { - "description": "Optional profile hint to the client to differentiate multiple MediaUrl objects from each other", - "type": "string" - } - } - }, - "AudioCard": { - "description": "A audio card", - "type": "object", - "properties": { - "aspect": { - "description": "Aspect ratio of thumbnail/media placeholder, allowed values are \"16x9\" and \"9x16\"", - "type": "string" - }, - "title": { - "description": "Title of the card", - "type": "string" - }, - "subtitle": { - "description": "Subtitle of the card", - "type": "string" - }, - "text": { - "description": "Text of the card", - "type": "string" - }, - "image": { - "$ref": "#/definitions/ThumbnailUrl", - "description": "Thumbnail placeholder" - }, - "media": { - "description": "Array of media Url objects", - "type": "array", - "items": { - "$ref": "#/definitions/MediaUrl" - } - }, - "buttons": { - "description": "Set of actions applicable to the current card", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - }, - "shareable": { - "description": "Is it OK for this content to be shareable with others (default:true)", - "type": "boolean" - }, - "autoloop": { - "description": "Should the client loop playback at end of content (default:true)", - "type": "boolean" - }, - "autostart": { - "description": "Should the client automatically start playback of video in this card (default:true)", - "type": "boolean" - } - } - }, - "ReceiptCard": { - "description": "A receipt card", - "type": "object", - "properties": { - "title": { - "description": "Title of the card", - "type": "string" - }, - "items": { - "description": "Array of Receipt Items", - "type": "array", - "items": { - "$ref": "#/definitions/ReceiptItem" - } - }, - "facts": { - "description": "Array of Fact Objects Array of key-value pairs.", - "type": "array", - "items": { - "$ref": "#/definitions/Fact" - } - }, - "tap": { - "$ref": "#/definitions/CardAction", - "description": "This action will be activated when user taps on the card" - }, - "total": { - "description": "Total amount of money paid (or should be paid)", - "type": "string" - }, - "tax": { - "description": "Total amount of TAX paid(or should be paid)", - "type": "string" - }, - "vat": { - "description": "Total amount of VAT paid(or should be paid)", - "type": "string" - }, - "buttons": { - "description": "Set of actions applicable to the current card", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - } - } - }, - "ReceiptItem": { - "description": "An item on a receipt card", - "type": "object", - "properties": { - "title": { - "description": "Title of the Card", - "type": "string" - }, - "subtitle": { - "description": "Subtitle appears just below Title field, differs from Title in font styling only", - "type": "string" - }, - "text": { - "description": "Text field appears just below subtitle, differs from Subtitle in font styling only", - "type": "string" - }, - "image": { - "$ref": "#/definitions/CardImage", - "description": "Image" - }, - "price": { - "description": "Amount with currency", - "type": "string" - }, - "quantity": { - "description": "Number of items of given kind", - "type": "string" - }, - "tap": { - "$ref": "#/definitions/CardAction", - "description": "This action will be activated when user taps on the Item bubble." - } - } - }, - "Fact": { - "description": "Set of key-value pairs. Advantage of this section is that key and value properties will be \r\n rendered with default style information with some delimiter between them. So there is no need for developer to specify style information.", - "type": "object", - "properties": { - "key": { - "description": "The key for this Fact", - "type": "string" - }, - "value": { - "description": "The value for this Fact", - "type": "string" - } - } - }, - "SigninCard": { - "description": "A card representing a request to sign in", - "type": "object", - "properties": { - "text": { - "description": "Text for signin request", - "type": "string" - }, - "buttons": { - "description": "Action to use to perform signin", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - } - } - }, - "ThumbnailCard": { - "description": "A thumbnail card (card with a single, small thumbnail image)", - "type": "object", - "properties": { - "title": { - "description": "Title of the card", - "type": "string" - }, - "subtitle": { - "description": "Subtitle of the card", - "type": "string" - }, - "text": { - "description": "Text for the card", - "type": "string" - }, - "images": { - "description": "Array of images for the card", - "type": "array", - "items": { - "$ref": "#/definitions/CardImage" - } - }, - "buttons": { - "description": "Set of actions applicable to the current card", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - }, - "tap": { - "$ref": "#/definitions/CardAction", - "description": "This action will be activated when user taps on the card itself" - } - } - }, - "VideoCard": { - "description": "A video card", - "type": "object", - "properties": { - "aspect": { - "description": "Aspect ratio (16:9)(4:3)", - "type": "string" - }, - "title": { - "description": "Title of the card", - "type": "string" - }, - "subtitle": { - "description": "Subtitle of the card", - "type": "string" - }, - "text": { - "description": "Text of the card", - "type": "string" - }, - "image": { - "$ref": "#/definitions/ThumbnailUrl", - "description": "Thumbnail placeholder" - }, - "media": { - "description": "Array of media Url objects", - "type": "array", - "items": { - "$ref": "#/definitions/MediaUrl" - } - }, - "buttons": { - "description": "Set of actions applicable to the current card", - "type": "array", - "items": { - "$ref": "#/definitions/CardAction" - } - }, - "shareable": { - "description": "Is it OK for this content to be shareable with others (default:true)", - "type": "boolean" - }, - "autoloop": { - "description": "Should the client loop playback at end of content (default:true)", - "type": "boolean" - }, - "autostart": { - "description": "Should the client automatically start playback of video in this card (default:true)", - "type": "boolean" - } - } - }, - "GeoCoordinates": { - "description": "GeoCoordinates (entity type: \"https://schema.org/GeoCoordinates\")", - "type": "object", - "properties": { - "elevation": { - "format": "double", - "description": "Elevation of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)", - "type": "number" - }, - "latitude": { - "format": "double", - "description": "Latitude of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)", - "type": "number" - }, - "longitude": { - "format": "double", - "description": "Longitude of the location [WGS 84](https://en.wikipedia.org/wiki/World_Geodetic_System)", - "type": "number" - }, - "type": { - "description": "The type of the thing", - "type": "string" - }, - "name": { - "description": "The name of the thing", - "type": "string" - } - } - }, - "Mention": { - "description": "Mention information (entity type: \"mention\")", - "type": "object", - "properties": { - "mentioned": { - "$ref": "#/definitions/ChannelAccount", - "description": "The mentioned user" - }, - "text": { - "description": "Sub Text which represents the mention (can be null or empty)", - "type": "string" - }, - "type": { - "description": "Entity Type (typically from schema.org types)", - "type": "string" - } - } - }, - "Place": { - "description": "Place (entity type: \"https://schema.org/Place\")", - "type": "object", - "properties": { - "address": { - "$ref": "#/definitions/Object", - "description": "Address of the place (may be `string` or complex object of type `PostalAddress`)" - }, - "geo": { - "$ref": "#/definitions/Object", - "description": "Geo coordinates of the place (may be complex object of type `GeoCoordinates` or `GeoShape`)" - }, - "hasMap": { - "$ref": "#/definitions/Object", - "description": "Map to the place (may be `string` (URL) or complex object of type `Map`)" - }, - "type": { - "description": "The type of the thing", - "type": "string" - }, - "name": { - "description": "The name of the thing", - "type": "string" - } - } - }, - "Thing": { - "description": "Thing (entity type: \"https://schema.org/Thing\")", - "type": "object", - "properties": { - "type": { - "description": "The type of the thing", - "type": "string" - }, - "name": { - "description": "The name of the thing", - "type": "string" - } - } - }, - "TokenParameters": { - "description": "Parameters for creating a token", - "type": "object", - "properties": { - "user": { - "$ref": "#/definitions/ChannelAccount", - "description": "User account to embed within the token" - }, - "eTag": { - "type": "string" - } - } - } - } -} \ No newline at end of file diff --git a/directline-3.0.json b/directline-3.0.json index 39755e98..ddb32a1d 100644 --- a/directline-3.0.json +++ b/directline-3.0.json @@ -1,5 +1,5 @@ { - "swagger": "2.0", + "openapi": "3.0.0", "info": { "version": "v3", "title": "Bot Connector - Direct Line API - v3.0", @@ -15,7 +15,7 @@ "url": "https://opensource.org/licenses/MIT" } }, - "host": "directline.botframework.com", + "servers": [{"url": "http://127.0.0.1:4242"}], "schemes": [ "https" ], diff --git a/package.json b/package.json index 03c5d47b..a5aa46a5 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "@azure/keyvault-keys": "4.8.0", "@azure/ms-rest-js": "2.7.0", "@azure/msal-node": "2.8.1", - "@azure/openai": "^2.0.0-beta.1", + "@azure/openai": "2.0.0-beta.1", "@azure/search-documents": "12.0.0", "@azure/storage-blob": "12.18.0", "@google-cloud/pubsub": "4.4.0", @@ -137,11 +137,12 @@ "google-libphonenumber": "3.2.34", "googleapis": "126.0.1", "hnswlib-node": "3.0.0", - "html-to-md": "^0.8.5", + "html-to-md": "0.8.5", "http-proxy": "1.18.1", "ibm-watson": "9.1.0", - "instagram-private-api": "^1.46.1", + "instagram-private-api": "1.46.1", "iso-639-1": "3.1.2", + "isomorphic-fetch": "3.0.0", "join-images-updated": "1.1.11", "js-md5": "0.8.3", "json-schema-to-zod": "2.1.0", @@ -210,7 +211,7 @@ "textract": "2.5.0", "twilio": "5.1.0", "twitter-api-v2": "1.17.0", - "typeorm": "^0.3.20", + "typeorm": "0.3.20", "typescript": "5.4.5", "url-join": "5.0.0", "vhost": "3.0.2", @@ -218,6 +219,7 @@ "vm2-process": "2.1.5", "walk-promise": "0.2.0", "washyourmouthoutwithsoap": "1.0.2", + "webdav-server": "2.6.2", "whatsapp-cloud-api": "0.3.1", "whatsapp-web.js": "https://github.com/Julzk/whatsapp-web.js/tarball/jkr_hotfix_7", "winston": "3.13.0", diff --git a/packages/basic.gblib/services/DebuggerService.ts b/packages/basic.gblib/services/DebuggerService.ts index 699a3f49..bc079acb 100644 --- a/packages/basic.gblib/services/DebuggerService.ts +++ b/packages/basic.gblib/services/DebuggerService.ts @@ -37,6 +37,7 @@ import SwaggerClient from 'swagger-client'; import { spawn } from 'child_process'; import { CodeServices } from '../../gpt.gblib/services/CodeServices.js'; import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; +import { GBUtil } from '../../../src/util.js'; /** * Web Automation services of conversation to be called by BASIC. @@ -154,12 +155,8 @@ export class DebuggerService { let min: GBMinInstance = GBServer.globals.minInstances.filter(p => p.instance.botId === botId)[0]; - const client = await new SwaggerClient({ - spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), - requestInterceptor: req => { - req.headers['Authorization'] = `Bearer ${min.instance.webchatKey}`; - } - }); + const client = await GBUtil.getDirectLineClient(min); + GBServer.globals.debuggers[botId].client = client; const response = await client.apis.Conversations.Conversations_StartConversation(); const conversationId = response.obj.conversationId; diff --git a/packages/basic.gblib/services/DialogKeywords.ts b/packages/basic.gblib/services/DialogKeywords.ts index cc968a7c..1350fca4 100644 --- a/packages/basic.gblib/services/DialogKeywords.ts +++ b/packages/basic.gblib/services/DialogKeywords.ts @@ -1348,12 +1348,7 @@ export class DialogKeywords { const conversation = min['apiConversations'][pid]; - const client = await new SwaggerClient({ - spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), - requestInterceptor: req => { - req.headers['Authorization'] = `Bearer ${min.instance.webchatKey}`; - } - }); + const client = await GBUtil.getDirectLineClient(min); conversation.client = client; const response = await client.apis.Conversations.Conversations_StartConversation(); conversation.conversationId = response.obj.conversationId; diff --git a/packages/core.gbapp/services/GBConfigService.ts b/packages/core.gbapp/services/GBConfigService.ts index b46d88aa..ae1a570a 100644 --- a/packages/core.gbapp/services/GBConfigService.ts +++ b/packages/core.gbapp/services/GBConfigService.ts @@ -106,7 +106,7 @@ export class GBConfigService { value = undefined; break; case 'STORAGE_FILE': - value = './guaribas.sqlite'; + value = './data.db'; break; case 'GBKB_AUTO_DEPLOY': value = false; diff --git a/packages/core.gbapp/services/GBMinService.ts b/packages/core.gbapp/services/GBMinService.ts index 0c82eec1..911326e6 100644 --- a/packages/core.gbapp/services/GBMinService.ts +++ b/packages/core.gbapp/services/GBMinService.ts @@ -42,6 +42,7 @@ import { FacebookAdapter } from 'botbuilder-adapter-facebook'; import mkdirp from 'mkdirp'; import Fs from 'fs'; import arrayBufferToBuffer from 'arraybuffer-to-buffer'; +import { v2 as webdav } from 'webdav-server'; import { NlpManager } from 'node-nlp'; import Koa from 'koa'; import { createRpcServer } from '@push-rpc/core'; @@ -253,6 +254,8 @@ export class GBMinService { GBServer.globals.minInstances.push(min); const user = null; // No user context. + // Serves individual URL for each bot conversational interface. + await this.deployer['deployPackage2'](min, user, 'packages/default.gbtheme'); // Install per bot deployed packages. @@ -313,6 +316,14 @@ export class GBMinService { if (!Fs.existsSync(dir)) { mkdirp.sync(dir); } + + dir = Path.join(process.env.PWD, 'work', gbai); + + const server = new webdav.WebDAVServer(); + server.setFileSystem(`/${botId}`, + new webdav.PhysicalFileSystem(dir), (success) => { + server.start(() => console.log('WEBDAV READY')); + }) // Loads Named Entity data for this bot. @@ -321,16 +332,11 @@ export class GBMinService { // Calls the loadBot context.activity for all packages. await this.invokeLoadBot(min.appPackages, GBServer.globals.sysPackages, min); - - // Serves individual URL for each bot conversational interface. - const receiver = async (req, res) => { await this.receiver(req, res, conversationState, min, instance, GBServer.globals.appPackages); }; let url = `/api/messages/${instance.botId}`; - GBServer.globals.server.post(url, receiver); - url = `/api/messages`; - GBServer.globals.server.post(url, receiver); + GBServer.globals.server.get(url, (req, res) => { if (req.query['hub.mode'] === 'subscribe') { if (req.query['hub.verify_token'] === process.env.FACEBOOK_VERIFY_TOKEN) { @@ -349,12 +355,7 @@ export class GBMinService { if (process.env.TEST_MESSAGE) { GBLogEx.info(min, `Starting auto test with '${process.env.TEST_MESSAGE}'.`); - const client = await new SwaggerClient({ - spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), - requestInterceptor: req => { - req.headers['Authorization'] = `Bearer ${min.instance.webchatKey}`; - } - }); + const client = await GBUtil.getDirectLineClient(min); const response = await client.apis.Conversations.Conversations_StartConversation(); const conversationId = response.obj.conversationId; @@ -450,7 +451,9 @@ export class GBMinService { whatsAppDirectLine = WhatsappDirectLine.botsByNumber[to]; } - await whatsAppDirectLine.WhatsAppCallback(req, res, whatsAppDirectLine.botId); + if (whatsAppDirectLine) { + await whatsAppDirectLine.WhatsAppCallback(req, res, whatsAppDirectLine.botId); + } }) .bind(min); @@ -672,27 +675,31 @@ export class GBMinService { theme = `default.gbtheme`; } - res.send( - JSON.stringify({ - instanceId: instance.instanceId, - botId: botId, - theme: theme, - //webchatToken: webchatTokenContainer.token, - domain: 'http://localhost:3978/directline', - speechToken: speechToken, - conversationId: webchatTokenContainer.conversationId, - authenticatorTenant: instance.authenticatorTenant, - authenticatorClientId: instance.marketplaceId, - paramLogoImageUrl: this.core.getParam(instance, 'Logo Image Url', null), - paramLogoImageAlt: this.core.getParam(instance, 'Logo Image Alt', null), - paramLogoImageWidth: this.core.getParam(instance, 'Logo Image Width', null), - paramLogoImageHeight: this.core.getParam(instance, 'Logo Image Height', null), - paramLogoImageType: this.core.getParam(instance, 'Logo Image Type', null), - logo: this.core.getParam(instance, 'Logo', null), - color1: this.core.getParam(instance, 'Color1', null), - color2: this.core.getParam(instance, 'Color2', null) - }) - ); + let config = { + instanceId: instance.instanceId, + botId: botId, + theme: theme, + speechToken: speechToken, + conversationId: webchatTokenContainer.conversationId, + authenticatorTenant: instance.authenticatorTenant, + authenticatorClientId: instance.marketplaceId, + paramLogoImageUrl: this.core.getParam(instance, 'Logo Image Url', null), + paramLogoImageAlt: this.core.getParam(instance, 'Logo Image Alt', null), + paramLogoImageWidth: this.core.getParam(instance, 'Logo Image Width', null), + paramLogoImageHeight: this.core.getParam(instance, 'Logo Image Height', null), + paramLogoImageType: this.core.getParam(instance, 'Logo Image Type', null), + logo: this.core.getParam(instance, 'Logo', null), + color1: this.core.getParam(instance, 'Color1', null), + color2: this.core.getParam(instance, 'Color2', null) + }; + + if (process.env.STORAGE_FILE) { + config['domain'] = `http://localhost:${process.env.PORT}/directline`; + } else { + config['webchatToken'] = webchatTokenContainer.token; + } + + res.send(JSON.stringify(config)); } else { const error = `Instance not found while retrieving from .gbui web client: ${botId}.`; res.sendStatus(error); @@ -749,15 +756,18 @@ export class GBMinService { */ private async buildBotAdapter(instance: any, sysPackages: IGBPackage[], appPackages: IGBPackage[]) { // MSFT stuff. - const uri = 'http://localhost:3978'; - const adapter = new BotFrameworkAdapter({ - clientOptions: { baseUri: uri }, + let config = { appId: instance.marketplaceId ? instance.marketplaceId : GBConfigService.get('MARKETPLACE_ID'), appPassword: instance.marketplacePassword ? instance.marketplacePassword : GBConfigService.get('MARKETPLACE_SECRET') - }); + }; + if (process.env.STORAGE_FILE) { + config['clientOptions'] = { baseUri: `http://localhost:${process.env.PORT}` }; + } + + const adapter = new BotFrameworkAdapter(config); const storage = new MemoryStorage(); const conversationState = new ConversationState(storage); const userState = new UserState(storage); @@ -770,6 +780,28 @@ export class GBMinService { // The minimal bot is built here. const min = new GBMinInstance(); + + // Setups default BOT Framework dialogs. + + min.userProfile = conversationState.createProperty('userProfile'); + const dialogState = conversationState.createProperty('dialogState'); + + min.dialogs = new DialogSet(dialogState); + min.dialogs.add(new TextPrompt('textPrompt')); + min.dialogs.add(new AttachmentPrompt('attachmentPrompt')); + + min.dialogs.add(new ConfirmPrompt('confirmPrompt')); + if (process.env.ENABLE_AUTH) { + min.dialogs.add( + new OAuthPrompt('oAuthPrompt', { + connectionName: 'OAuth2', + text: 'Please sign in to General Bots.', + title: 'Sign in', + timeout: 300000 + }) + ); + } + min.botId = instance.botId; min.bot = adapter; min.userState = userState; @@ -792,6 +824,15 @@ export class GBMinService { min['apiConversations'] = {}; min.packages = sysPackages; + const receiver = async (req, res) => { + await this.receiver(req, res, conversationState, min, instance, GBServer.globals.appPackages); + }; + + let url = `/api/messages/${instance.botId}`; + GBServer.globals.server.post(url, receiver); + url = `/api/messages`; + GBServer.globals.server.post(url, receiver); + // NLP Manager. const manager = new NlpManager({ languages: ['pt'], forceNER: true }); @@ -879,26 +920,6 @@ export class GBMinService { WhatsappDirectLine.botsByNumber[botNumber] = min.whatsAppDirectLine; } - // Setups default BOT Framework dialogs. - - min.userProfile = conversationState.createProperty('userProfile'); - const dialogState = conversationState.createProperty('dialogState'); - - min.dialogs = new DialogSet(dialogState); - min.dialogs.add(new TextPrompt('textPrompt')); - min.dialogs.add(new AttachmentPrompt('attachmentPrompt')); - - min.dialogs.add(new ConfirmPrompt('confirmPrompt')); - if (process.env.ENABLE_AUTH) { - min.dialogs.add( - new OAuthPrompt('oAuthPrompt', { - connectionName: 'OAuth2', - text: 'Please sign in to General Bots.', - title: 'Sign in', - timeout: 300000 - }) - ); - } return { min, adapter, conversationState }; } @@ -1177,7 +1198,7 @@ export class GBMinService { await handler(context); // Return status res.status(200); - + res.end(); } else { await adapter['processActivity'](req, res, handler); @@ -1376,7 +1397,7 @@ export class GBMinService { context.activity.text = context.activity.text.trim(); const member = context.activity.from; - let memberId, email; + let memberId = null, email = null; // Processes e-mail from id in case of Teams messages. @@ -1648,12 +1669,7 @@ export class GBMinService { if (script === 'start') { pid = GBVMService.createProcessInfo(user, min, 'api', null); - const client = await new SwaggerClient({ - spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), - requestInterceptor: req => { - req.headers['Authorization'] = `Bearer ${min.instance.webchatKey}`; - } - }); + const client = await GBUtil.getDirectLineClient(min); const response = await client.apis.Conversations.Conversations_StartConversation(); min['apiConversations'][pid] = { conversation: response.obj, client: client }; diff --git a/packages/core.gbapp/services/router/bridge.ts b/packages/core.gbapp/services/router/bridge.ts new file mode 100644 index 00000000..25ed4d34 --- /dev/null +++ b/packages/core.gbapp/services/router/bridge.ts @@ -0,0 +1,322 @@ +import bodyParser from 'body-parser'; +import express from 'express'; +import fetch from 'isomorphic-fetch'; +import moment from 'moment'; +import * as uuidv4 from 'uuid'; + +import { IActivity, IBotData, IConversation, IConversationUpdateActivity, IMessageActivity } from './types'; + +const expiresIn = 1800; +const conversationsCleanupInterval = 10000; +const conversations: { [key: string]: IConversation } = {}; +const botDataStore: { [key: string]: IBotData } = {}; + +export const getRouter = (serviceUrl: string, botUrl: string, conversationInitRequired = true): express.Router => { + const router = express.Router(); + + router.use(bodyParser.json()); // for parsing application/json + router.use(bodyParser.urlencoded({ extended: true })); // for parsing application/x-www-form-urlencoded + router.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, PUT, POST, DELETE, PATCH, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization, x-ms-bot-agent'); + next(); + }); + // CLIENT ENDPOINT + router.options('/directline', (req, res) => { + res.status(200).end(); + }); + + // Creates a conversation + const reqs = (req, res) => { + + const conversationId: string = uuidv4.v4().toString(); + conversations[conversationId] = { + conversationId, + history: [], + }; + console.log('Created conversation with conversationId: ' + conversationId); + + const activity = createConversationUpdateActivity(serviceUrl, conversationId); + fetch(botUrl, { + method: 'POST', + body: JSON.stringify(activity), + headers: { + 'Content-Type': 'application/json', + }, + }).then((response) => { + res.status(response.status).send({ + conversationId, + expiresIn, + }); + }); + }; + + router.post('/v3/directline/conversations',reqs ); + router.post('/directline/conversations',reqs ); + + // Reconnect API + router.get('/v3/directline/conversations/:conversationId', (req, res) => { + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + if (conversation) { + res.status(200).send(conversation); + } else { + // Conversation was never initialized + res.status(400).send(); + } + + console.warn('/v3/directline/conversations/:conversationId not implemented'); + }); + + // Gets activities from store (local history array for now) + router.get('/directline/conversations/:conversationId/activities', (req, res) => { + const watermark = req.query.watermark && req.query.watermark !== 'null' ? Number(req.query.watermark) : 0; + + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + + if (conversation) { + // If the bot has pushed anything into the history array + if (conversation.history.length > watermark) { + const activities = conversation.history.slice(watermark); + res.status(200).json({ + activities, + watermark: watermark + activities.length, + }); + } else { + res.status(200).send({ + activities: [], + watermark, + }); + } + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); + + // Sends message to bot. Assumes message activities + router.post('/directline/conversations/:conversationId/activities', (req, res) => { + const incomingActivity = req.body; + // Make copy of activity. Add required fields + const activity = createMessageActivity(incomingActivity, serviceUrl, req.params.conversationId); + + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + + if (conversation) { + conversation.history.push(activity); + fetch(botUrl, { + method: 'POST', + body: JSON.stringify(activity), + headers: { + 'Content-Type': 'application/json', + }, + }).then((response) => { + res.status(response.status).json({ id: activity.id }); + }); + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); + + router.post('/v3/directline/conversations/:conversationId/upload', (req, res) => { console.warn('/v3/directline/conversations/:conversationId/upload not implemented'); }); + router.get('/v3/directline/conversations/:conversationId/stream', (req, res) => { console.warn('/v3/directline/conversations/:conversationId/stream not implemented'); }); + + // BOT CONVERSATION ENDPOINT + + router.post('/v3/conversations', (req, res) => { console.warn('/v3/conversations not implemented'); }); + + router.post('/v3/conversations/:conversationId/activities', (req, res) => { + let activity: IActivity; + + activity = req.body; + activity.id = uuidv4.v4(); + activity.from = { id: 'id', name: 'Bot' }; + + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + if (conversation) { + conversation.history.push(activity); + res.status(200).send(); + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); + + router.post('/v3/conversations/:conversationId/activities/:activityId', (req, res) => { + let activity: IActivity; + + activity = req.body; + activity.id = uuidv4.v4(); + activity.from = { id: 'id', name: 'Bot' }; + + const conversation = getConversation(req.params.conversationId, conversationInitRequired); + if (conversation) { + conversation.history.push(activity); + res.status(200).send(); + } else { + // Conversation was never initialized + res.status(400).send(); + } + }); + + router.get('/v3/conversations/:conversationId/members', (req, res) => { console.warn('/v3/conversations/:conversationId/members not implemented'); }); + router.get('/v3/conversations/:conversationId/activities/:activityId/members', (req, res) => { console.warn('/v3/conversations/:conversationId/activities/:activityId/members'); }); + + // BOTSTATE ENDPOINT + + router.get('/v3/botstate/:channelId/users/:userId', (req, res) => { + console.log('Called GET user data'); + getBotData(req, res); + }); + + router.get('/v3/botstate/:channelId/conversations/:conversationId', (req, res) => { + console.log(('Called GET conversation data')); + getBotData(req, res); + }); + + router.get('/v3/botstate/:channelId/conversations/:conversationId/users/:userId', (req, res) => { + console.log('Called GET private conversation data'); + getBotData(req, res); + }); + + router.post('/v3/botstate/:channelId/users/:userId', (req, res) => { + console.log('Called POST setUserData'); + setUserData(req, res); + }); + + router.post('/v3/botstate/:channelId/conversations/:conversationId', (req, res) => { + console.log('Called POST setConversationData'); + setConversationData(req, res); + }); + + router.post('/v3/botstate/:channelId/conversations/:conversationId/users/:userId', (req, res) => { + setPrivateConversationData(req, res); + }); + + router.delete('/v3/botstate/:channelId/users/:userId', (req, res) => { + console.log('Called DELETE deleteStateForUser'); + deleteStateForUser(req, res); + }); + + return router; +}; + +/** + * @param app The express app where your offline-directline endpoint will live + * @param port The port where your offline-directline will be hosted + * @param botUrl The url of the bot (e.g. http://127.0.0.1:3978/api/messages) + * @param conversationInitRequired Requires that a conversation is initialized before it is accessed, returning a 400 + * when not the case. If set to false, a new conversation reference is created on the fly. This is true by default. + */ +export const initializeRoutes = (app: express.Express, port: number, botUrl: string, conversationInitRequired = true) => { + conversationsCleanup(); + + const directLineEndpoint = `http://127.0.0.1:${port}`; + const router = getRouter(directLineEndpoint, botUrl, conversationInitRequired); + + app.use(router); + console.log(`Routing messages to bot on ${botUrl}`); + +}; + +const getConversation = (conversationId: string, conversationInitRequired: boolean) => { + + // Create conversation on the fly when needed and init not required + if (!conversations[conversationId] && !conversationInitRequired) { + conversations[conversationId] = { + conversationId, + history: [], + }; + } + return conversations[conversationId]; +}; + +const getBotDataKey = (channelId: string, conversationId: string, userId: string) => { + return `$${channelId || '*'}!${conversationId || '*'}!${userId || '*'}`; +}; + +const setBotData = (channelId: string, conversationId: string, userId: string, incomingData: IBotData): IBotData => { + const key = getBotDataKey(channelId, conversationId, userId); + const newData: IBotData = { + eTag: new Date().getTime().toString(), + data: incomingData.data, + }; + + if (incomingData) { + botDataStore[key] = newData; + } else { + delete botDataStore[key]; + newData.eTag = '*'; + } + + return newData; +}; + +const getBotData = (req: express.Request, res: express.Response) => { + const key = getBotDataKey(req.params.channelId, req.params.conversationId, req.params.userId); + console.log('Data key: ' + key); + + res.status(200).send(botDataStore[key] || { data: null, eTag: '*' }); +}; + +const setUserData = (req: express.Request, res: express.Response) => { + res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); +}; + +const setConversationData = (req: express.Request, res: express.Response) => { + res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); +}; + +const setPrivateConversationData = (req: express.Request, res: express.Response) => { + res.status(200).send(setBotData(req.params.channelId, req.params.conversationId, req.params.userId, req.body)); +}; + +export const start = (server)=>{ + + initializeRoutes(server, Number(process.env.PORT), `http://127.0.0.1:${process.env.PORT}/api/messages`); +} + +const deleteStateForUser = (req: express.Request, res: express.Response) => { + Object.keys(botDataStore) + .forEach((key) => { + if (key.endsWith(`!{req.query.userId}`)) { + delete botDataStore[key]; + } + }); + res.status(200).send(); +}; + +// CLIENT ENDPOINT HELPERS +const createMessageActivity = (incomingActivity: IMessageActivity, serviceUrl: string, conversationId: string): IMessageActivity => { + return { ...incomingActivity, channelId: 'emulator', serviceUrl, conversation: { id: conversationId }, id: uuidv4.v4() }; +}; + +const createConversationUpdateActivity = (serviceUrl: string, conversationId: string): IConversationUpdateActivity => { + const activity: IConversationUpdateActivity = { + type: 'conversationUpdate', + channelId: 'emulator', + serviceUrl, + conversation: { id: conversationId }, + id: uuidv4.v4(), + membersAdded: [], + membersRemoved: [], + from: { id: 'offline-directline', name: 'Offline Directline Server' }, + }; + return activity; +}; + +const conversationsCleanup = () => { + setInterval(() => { + const expiresTime = moment().subtract(expiresIn, 'seconds'); + Object.keys(conversations).forEach((conversationId) => { + if (conversations[conversationId].history.length > 0) { + const lastTime = moment(conversations[conversationId].history[conversations[conversationId].history.length - 1].localTimestamp); + if (lastTime < expiresTime) { + delete conversations[conversationId]; + console.log('deleted cId: ' + conversationId); + } + } + }); + }, conversationsCleanupInterval); +}; diff --git a/packages/core.gbapp/services/router/types.ts b/packages/core.gbapp/services/router/types.ts new file mode 100644 index 00000000..f6e6f4a9 --- /dev/null +++ b/packages/core.gbapp/services/router/types.ts @@ -0,0 +1,66 @@ +export interface IUser { + id: string, + name: string +} + +export interface IChannelAccount { + id?: string, + name?: string, +} + +export interface IConversationAccount extends IChannelAccount { + isGroup?: boolean, +} + +export interface IAttachment { + contentType?: string, + contentUrl?: string, + content?: any, + name?: string, + thumbnailUrl?: string, +} + +export interface IEntity { + type?: string, +} + +export interface IActivity { + type?: string, + id?: string, + serviceUrl?: string, + timestamp?: string, + localTimestamp?: string, + channelId?: string, + from?: IChannelAccount, + conversation?: IConversationAccount, + recipient?: IChannelAccount, + replyToId?: string, + channelData?: any, +} + +export interface IMessageActivity extends IActivity { + locale?: string, + text?: string, + summary?: string, + textFormat?: string, + attachmentLayout?: string, + attachments?: IAttachment[], + entities?: IEntity[], +} + +export interface IBotData { + eTag: string; + data: any; +} + +export interface IConversation { + conversationId: string, + history?: IActivity[] +} + +export interface IConversationUpdateActivity extends IActivity { + membersAdded?: IChannelAccount[], + membersRemoved?: IChannelAccount[], + topicName?: string, + historyDisclosed?: boolean, +} \ No newline at end of file diff --git a/packages/google-chat.gblib/services/GoogleChatDirectLine.ts b/packages/google-chat.gblib/services/GoogleChatDirectLine.ts index a31589a4..6b979687 100644 --- a/packages/google-chat.gblib/services/GoogleChatDirectLine.ts +++ b/packages/google-chat.gblib/services/GoogleChatDirectLine.ts @@ -36,6 +36,7 @@ import { GBLog, GBMinInstance, GBService } from 'botlib'; import { GBServer } from '../../../src/app.js'; import { SecService } from '../../security.gbapp/services/SecService.js'; import { GBLogEx } from '../../core.gbapp/services/GBLogEx.js'; +import { GBUtil } from '../../../src/util.js'; /** * Support for Google Chat. @@ -90,10 +91,7 @@ export class GoogleChatDirectLine extends GBService { } public async setup (setUrl) { - this.directLineClient = new Swagger({ - spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), - usePromise: true - }); + this.directLineClient = await GBUtil.getDirectLineClient(this.min); const client = await this.directLineClient; client.clientAuthorizations.add( diff --git a/packages/security.gbapp/services/SecService.ts b/packages/security.gbapp/services/SecService.ts index ad699c4b..55ab9074 100644 --- a/packages/security.gbapp/services/SecService.ts +++ b/packages/security.gbapp/services/SecService.ts @@ -54,8 +54,10 @@ export class SecService extends GBService { user.email = email; user.defaultChannel = channelName; GBServer.globals.users [user.userId] = user; - - return await user.save(); + if(user.changed()){ + await user.save(); + } + return user; } /** diff --git a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts index e9391428..d426802c 100644 --- a/packages/whatsapp.gblib/services/WhatsappDirectLine.ts +++ b/packages/whatsapp.gblib/services/WhatsappDirectLine.ts @@ -30,7 +30,6 @@ import mime from 'mime-types'; import urlJoin from 'url-join'; -import SwaggerClient from 'swagger-client'; import Path from 'path'; import Fs from 'fs'; import { GBLog, GBMinInstance, GBService, IGBPackage } from 'botlib'; @@ -46,7 +45,7 @@ import qrcode from 'qrcode-terminal'; import express from 'express'; import { GBSSR } from '../../core.gbapp/services/GBSSR.js'; import pkg from 'whatsapp-web.js'; -import fetch, { Response } from 'node-fetch'; +import fetch from 'node-fetch'; import { DialogKeywords } from '../../basic.gblib/services/DialogKeywords.js'; import { ChatServices } from '../../gpt.gblib/services/ChatServices.js'; import { GBAdminService } from '../../admin.gbapp/services/GBAdminService.js'; @@ -117,20 +116,9 @@ export class WhatsappDirectLine extends GBService { } public async setup(setUrl: boolean) { - const client = await new SwaggerClient({ - url: 'http://127.0.0.1:3978/api/messages', // TODO: - spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), - requestInterceptor: req => { - req.headers['Authorization'] = `Bearer ${this.min.instance.webchatKey}`; - } - }); - this.directLineClient = client; + this.directLineClient = GBUtil.getDirectLineClient(this.min); - // Warms up MSBF. - - await client.apis.Conversations.Conversations_StartConversation(); - let url: string; let options: any; diff --git a/src/app.ts b/src/app.ts index cd95764f..8806eb3a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -40,6 +40,8 @@ import bodyParser from 'body-parser'; import { GBLog, GBMinInstance, IGBCoreService, IGBInstance } from 'botlib'; import child_process from 'child_process'; import express from 'express'; +import {start as startRouter} from '../packages/core.gbapp/services/router/bridge.js' + import fs from 'fs'; import http from 'http'; import httpProxy from 'http-proxy'; @@ -87,8 +89,9 @@ export class GBServer { const server = express(); this.initEndpointsDocs(server); - + startRouter(server); GBServer.globals.server = server; + GBServer.globals.httpsServer = null; GBServer.globals.webSessions = {}; GBServer.globals.processes = {}; @@ -118,7 +121,7 @@ export class GBServer { }); process.on('uncaughtException', (err, p) => { - GBLogEx.error(0, `GBEXCEPTION: ${GBUtil.toYAML(err)} ${GBUtil.toYAML(p)}`); + GBLogEx.error(0, `GBEXCEPTION: ${GBUtil.toYAML(JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))))}`); }); process.on('unhandledRejection', (err, p) => { @@ -131,7 +134,7 @@ export class GBServer { } if (!bypass) { - GBLogEx.error(0, `GBREJECTION: ${GBUtil.toYAML(err)} ${GBUtil.toYAML(p)}`); + GBLogEx.error(0,`GBREJECTION: ${GBUtil.toYAML(JSON.parse(JSON.stringify(err, Object.getOwnPropertyNames(err))))}`); } }); @@ -147,6 +150,7 @@ export class GBServer { (async () => { try { GBLogEx.info(0, `Now accepting connections on ${port}...`); + process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0'; // Reads basic configuration, initialize minimal services. @@ -247,7 +251,7 @@ export class GBServer { const minService: GBMinService = new GBMinService(core, conversationalService, adminService, deployer); GBServer.globals.minService = minService; await minService.buildMin(instances); - + server.all('*', async (req, res, next) => { const host = req.headers.host; diff --git a/src/util.ts b/src/util.ts index ac3cb70e..fb123469 100644 --- a/src/util.ts +++ b/src/util.ts @@ -34,6 +34,8 @@ 'use strict'; import * as YAML from 'yaml'; +import SwaggerClient from 'swagger-client'; +import Fs from 'fs'; export class GBUtil { public static repeat(chr, count) { @@ -53,7 +55,7 @@ export class GBUtil { return (GBUtil.repeat(pad, length) + value).substr(0, width); } - + public static padR(value, width, pad) { if (!width || width < 1) return value; @@ -64,6 +66,22 @@ export class GBUtil { return (value + GBUtil.repeat(pad, length)).substr(0, width); } + public static async getDirectLineClient(min) { + + let config = { + url: `http://127.0.0.1:${process.env.port}/api/messages`, + spec: JSON.parse(Fs.readFileSync('directline-3.0.json', 'utf8')), + requestInterceptor: req => { + req.headers['Authorization'] = `Bearer ${min.instance.webchatKey}`; + } + }; + if (process.env.STORAGE_FILE) { + config['spec'].servers = [{ url: `http://127.0.0.1:${process.env.PORT}/api/messages` }]; + config['openapi'] = '3.0.0'; + } + return await new SwaggerClient(config); + } + public static toYAML(json) { const doc = new YAML.Document(); doc.contents = json;