diff --git a/Node/core-SendAttachment/README.md b/Node/core-SendAttachment/README.md index 76a5d57a20..0a30fe332b 100644 --- a/Node/core-SendAttachment/README.md +++ b/Node/core-SendAttachment/README.md @@ -19,34 +19,115 @@ Many messaging channels provide the ability to attach richer objects. Bot Builde * **Media and Files**: Basic files can be sent by setting [contentType](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.iattachment.html#contenttype) to the MIME type of the file and then passing a link to the file in [contentUrl](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.iattachment.html#contenturl). * **Cards and Keyboards**: A rich set of visual cards and custom keyboards can by setting [contentType](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.iattachment.html#contenttype) to the cards type and then passing the JSON for the card in [content](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.iattachment.html#content). If you use one of the rich card builder classes like [HeroCard](https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.herocard.html) the attachment will automatically filled in for you. +As a developer, you have three ways to send the attachment. The attachment can be: + - An inline file, by encoding the file as base64 and use it in the contentUrl + - A file uploaded to the channel's store via the Connection API, then using the attachmentId to create the contentUrl + - An externally hosted file, by just specifying the Url of the file (it should be publicly accessible) + +#### Attaching the image inline + +It consists on sending the file contents, encoded in base64, along with the message payload. This option works for small files, like icon size images. +You'll need to encode file's content, then set the attachment's `contentUrl` as follows: + +```` +data:image/png;base64,iVBORw0KGgo… +```` + +Checkout [app.js](./app.js#L64-L73) to see how to convert a file read using `fs.readFile()` and then create the message attachment. + ````JavaScript -function (session) { - - // Create and send attachment - var attachment = { - contentUrl: "https://docs.botframework.com/en-us/images/faq-overview/botframework_overview_july.png", - contentType: "image/png", - name: "BotFrameworkOverview.png" - }; +fs.readFile('./images/small-image.png', (err, data) => { + var contentType = 'image/png'; + var base64 = Buffer.from(data).toString('base64'); var msg = new builder.Message(session) - .addAttachment(attachment); + .addAttachment({ + contentUrl: util.format('data:%s;base64,%s', contentType, base64), + contentType: contentType, + name: 'BotFrameworkLogo.png' + }); session.send(msg); -} +}); +```` + +#### Uploading the file via the Connector API + +This option should be used when the file to send is less than 256Kb in size when encoded to base64. A good scenario are images generated based on user input. +It does require a few more steps than the other methods, but leverages the channels store to store the file: + +0. Read (or generate) the content file and store it in a Buffer for encoding to base64 ([relevant code](./app.js#L127)) +1. Create a client to the Connector API ([relevant code](./app.js#L9-L14)) +2. Inject the Bot Connector's token into the Connector API client ([relevant code](./app.js#L147)) +3. Set the Connector API client service url to the Connector's ([relevant code](./app.js#L148-L151)) +4. Upload the base64 encoded payload to the conversations/attachments endpoint ([relevant code](./app.js#L153-L163)) +5. Use the returned attachmentId to generate the contentUrl ([relevant code](./app.js#L165-L169)) + +This sample provides a [helper method](./app.js#L124-L172) you can use that encapsulates most of the previous steps. + +````JavaScript +// read file content and upload +fs.readFile('./images/big-image.png', (err, data) => { + if (err) { + return session.send('Oops. Error reading file.'); + } + + // Upload file data using helper function + uploadAttachment( + data, + 'image/png', + 'BotFrameworkImage.png', + connector, + connectorApiClient, + session.message.address.serviceUrl, + session.message.address.conversation.id) + .then(attachmentUrl => { + // Send Message with Attachment obj using returned Url + var msg = new builder.Message(session) + .addAttachment({ + contentUrl: attachmentUrl, + contentType: 'image/png', + name: 'BotFrameworkLogo.png' + }); + + session.send(msg); + }) + .catch(err => { + console.log('Error uploading file', err); + session.send('Oops. Error uploading file. ' + err.message); + }); +}); +```` + +#### Using an externally hosted file + +This option is the simplest but requires the image to be already on the Internet and be publicly accesible. +You could also provide an Url pointing to your own site. + +Checkout [app.js](./app.js#L114-L121) to see how to create a message with a single image attachment. + +````JavaScript +var msg = new builder.Message(session) + .addAttachment({ + contentUrl: 'https://docs.botframework.com/en-us/images/faq-overview/botframework_overview_july.png', + contentType: 'image/png', + name: 'BotFrameworkOverview.png' + }); + +session.send(msg); ```` ### Outcome -You will see the following in the Bot Framework Emulator when opening and running the sample solution. +You will see the following in the Bot Framework Emulator when selecting the inline attachment. See how the image is encoded in the `contentUrl` of the attachment. ![Sample Outcome](images/outcome-emulator.png) -You will see the following in your Facebook Messenger. +You will see the following in your Facebook Messenger when selecting to upload the attachment. ![Sample Outcome](images/outcome-facebook.png) -On the other hand, you will see the following in Skype. +On the other hand, you will see the following in Skype when selecting an Internet attachment. ![Sample Outcome](images/outcome-skype.png) @@ -57,3 +138,4 @@ To get more information about how to get started in Bot Builder for Node and Att * [Adding Attachments to a Message](https://docs.botframework.com/en-us/core-concepts/attachments) * [Attachments](https://docs.botframework.com/en-us/node/builder/chat-reference/interfaces/_botbuilder_d_.iattachment.html) * [Message.addAttachment method](https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.message.html#addattachment) +* [Connector API - UploadAttachment](https://docs.botframework.com/en-us/restapi/connector/#!/Conversations/Conversations_UploadAttachment) diff --git a/Node/core-SendAttachment/app.js b/Node/core-SendAttachment/app.js index 345652640f..1e7bcd7d68 100644 --- a/Node/core-SendAttachment/app.js +++ b/Node/core-SendAttachment/app.js @@ -1,5 +1,17 @@ var builder = require('botbuilder'); var restify = require('restify'); +var Swagger = require('swagger-client'); +var Promise = require('bluebird'); +var url = require('url'); +var fs = require('fs'); +var util = require('util'); + +// Swagger client for Bot Connector API +var connectorApiClient = new Swagger( + { + url: 'https://raw.githubusercontent.com/Microsoft/BotBuilder/master/CSharp/Library/Microsoft.Bot.Connector/Swagger/ConnectorAPI.json', + usePromise: true + }); // Setup Restify Server var server = restify.createServer(); @@ -16,17 +28,145 @@ var connector = new builder.ChatConnector({ // Listen for messages server.post('/api/messages', connector.listen()); -var bot = new builder.UniversalBot(connector, function (session) { - - // Create and send attachment - var attachment = { - contentUrl: 'https://docs.botframework.com/en-us/images/faq-overview/botframework_overview_july.png', - contentType: 'image/png', - name: 'BotFrameworkOverview.png' - }; +// Bot Dialogs +var bot = new builder.UniversalBot(connector, [ + function (session) { + session.send('Welcome, here you can see attachment alternatives:'); + builder.Prompts.choice(session, 'What sample option would you like to see?', Options, { + maxRetries: 3 + }); + }, + function (session, results) { + var option = results.response ? results.response.entity : Inline; + switch (option) { + case Inline: + return sendInline(session, './images/small-image.png', 'image/png', 'BotFrameworkLogo.png'); + case Upload: + return uploadFileAndSend(session, './images/big-image.png', 'image/png', 'BotFramework.png'); + case External: + var url = 'https://docs.botframework.com/en-us/images/faq-overview/botframework_overview_july.png'; + return sendInternetUrl(session, url, 'image/png', 'BotFrameworkOverview.png'); + } + }]); + +const Inline = 'Show inline attachment'; +const Upload = 'Show uploaded attachment'; +const External = 'Show Internet attachment'; +const Options = [Inline, Upload, External]; + +// Sends attachment inline in base64 +function sendInline(session, filePath, contentType, attachmentFileName) { + fs.readFile(filePath, (err, data) => { + if (err) { + return session.send('Oops. Error reading file.'); + } + + var base64 = Buffer.from(data).toString('base64'); + + var msg = new builder.Message(session) + .addAttachment({ + contentUrl: util.format('data:%s;base64,%s', contentType, base64), + contentType: contentType, + name: attachmentFileName + }); + + session.send(msg); + }); +} + +// Uploads a file using the Connector API and sends attachment +function uploadFileAndSend(session, filePath, contentType, attachmentFileName) { + + // read file content and upload + fs.readFile(filePath, (err, data) => { + if (err) { + return session.send('Oops. Error reading file.'); + } + + // Upload file data using helper function + uploadAttachment( + data, + contentType, + attachmentFileName, + connector, + connectorApiClient, + session.message.address.serviceUrl, + session.message.address.conversation.id) + .then(attachmentUrl => { + // Send Message with Attachment obj using returned Url + var msg = new builder.Message(session) + .addAttachment({ + contentUrl: attachmentUrl, + contentType: contentType, + name: attachmentFileName + }); + session.send(msg); + }) + .catch(err => { + console.log('Error uploading file', err); + session.send('Oops. Error uploading file. ' + err.message); + }); + }); +} +// Sends attachment using an Internet url +function sendInternetUrl(session, url, contentType, attachmentFileName) { var msg = new builder.Message(session) - .addAttachment(attachment); + .addAttachment({ + contentUrl: url, + contentType: contentType, + name: attachmentFileName + }); session.send(msg); -}); \ No newline at end of file +} + +// Uploads file to Connector API and returns Attachment URLs +function uploadAttachment(fileData, contentType, fileName, connector, connectorApiClient, baseServiceUrl, conversationId) { + + var base64 = Buffer.from(fileData).toString('base64'); + + // Inject the conenctor's JWT token into to the Swagger client + function addTokenToClient(connector, clientPromise) { + // ask the connector for the token. If it expired, a new token will be requested to the API + var obtainToken = Promise.promisify(connector.addAccessToken.bind(connector)); + var options = {}; + return Promise.all([clientPromise, obtainToken(options)]).then((values) => { + var client = values[0]; + var hasToken = !!options.headers.Authorization; + if (hasToken) { + var authHeader = options.headers.Authorization; + client.clientAuthorizations.add('AuthorizationBearer', new Swagger.ApiKeyAuthorization('Authorization', authHeader, 'header')); + } + + return client; + }); + } + + // 1. inject the JWT from the connector to the client on every call + return addTokenToClient(connector, connectorApiClient).then((client) => { + // 2. override API client host (api.botframework.com) with channel's serviceHost (e.g.: slack.botframework.com) + var serviceUrl = url.parse(baseServiceUrl); + var serviceHost = serviceUrl.host; + client.setHost(serviceHost); + + // 3. POST /v3/conversations/{conversationId}/attachments + var uploadParameters = { + conversationId: conversationId, + attachmentUpload: { + type: contentType, + name: fileName, + originalBase64: base64 + } + }; + + return client.Conversations.Conversations_UploadAttachment(uploadParameters) + .then((res) => { + var attachmentId = res.obj.id; + var attachmentUrl = serviceUrl; + + attachmentUrl.pathname = util.format('/v3/attachments/%s/views/%s', attachmentId, 'original'); + return attachmentUrl.format(); + }); + }); +} \ No newline at end of file diff --git a/Node/core-SendAttachment/images/big-image.png b/Node/core-SendAttachment/images/big-image.png new file mode 100644 index 0000000000..4b52a6a17a Binary files /dev/null and b/Node/core-SendAttachment/images/big-image.png differ diff --git a/Node/core-SendAttachment/images/outcome-emulator.png b/Node/core-SendAttachment/images/outcome-emulator.png index caef27eb31..c97f14fa13 100644 Binary files a/Node/core-SendAttachment/images/outcome-emulator.png and b/Node/core-SendAttachment/images/outcome-emulator.png differ diff --git a/Node/core-SendAttachment/images/outcome-facebook.png b/Node/core-SendAttachment/images/outcome-facebook.png index 1f5afcdfa0..40c94e4652 100644 Binary files a/Node/core-SendAttachment/images/outcome-facebook.png and b/Node/core-SendAttachment/images/outcome-facebook.png differ diff --git a/Node/core-SendAttachment/images/outcome-skype.png b/Node/core-SendAttachment/images/outcome-skype.png index 54c04358dc..db098ca63a 100644 Binary files a/Node/core-SendAttachment/images/outcome-skype.png and b/Node/core-SendAttachment/images/outcome-skype.png differ diff --git a/Node/core-SendAttachment/images/small-image.png b/Node/core-SendAttachment/images/small-image.png new file mode 100644 index 0000000000..703bbbcfa9 Binary files /dev/null and b/Node/core-SendAttachment/images/small-image.png differ diff --git a/Node/core-SendAttachment/package.json b/Node/core-SendAttachment/package.json index 4172e09175..ee5cd26093 100644 --- a/Node/core-SendAttachment/package.json +++ b/Node/core-SendAttachment/package.json @@ -21,7 +21,9 @@ "url": "https://github.com/Microsoft/BotBuilder-Samples.git" }, "dependencies": { + "bluebird": "^3.4.7", "botbuilder": "^3.5.1", - "restify": "^4.3.0" + "restify": "^4.3.0", + "swagger-client": "^2.1.32" } }