From e37aa2f3ea66aca07724a2a3008c946867ed132d Mon Sep 17 00:00:00 2001 From: Ivan Jovanovic Date: Sun, 4 Dec 2016 17:19:17 +0100 Subject: [PATCH] Skype format message (#58) * Skype format messages, added Photo * Initialize Skype format message * Fix issue * Added Carousel format and Hero inside it * Thumbnail skype format message * Smaller fixes skype format message * Skype receipt * Fixes and updates * Skype format message tests and error handlers * Added Skype button type * Added addTitle, addSubtitle, addText, increased test coverage and refactored a bit * Skype custom messages documentation * Skype custom messages documentation fixed some typos --- docs/SKYPE_CUSTOM_MESSAGES.md | 280 ++++++++++++++++++++++ lib/bot-builder.js | 2 + lib/skype/format-message.js | 203 ++++++++++++++++ spec/skype/skype-format-message-spec.js | 298 ++++++++++++++++++++++++ 4 files changed, 783 insertions(+) create mode 100644 docs/SKYPE_CUSTOM_MESSAGES.md create mode 100644 lib/skype/format-message.js create mode 100644 spec/skype/skype-format-message-spec.js diff --git a/docs/SKYPE_CUSTOM_MESSAGES.md b/docs/SKYPE_CUSTOM_MESSAGES.md new file mode 100644 index 0000000..d27be5e --- /dev/null +++ b/docs/SKYPE_CUSTOM_MESSAGES.md @@ -0,0 +1,280 @@ +# Skype Custom Messages and Message Templates + +In this guide: + +1. [Intro](#intro) +2. [Text messages](#text-messages) +3. [Emoticon messages](#emoticon-messages) +4. [Photo messages](#photo-messages) +5. [Carousel messages](#carousel-messages) + 1. [Hero](#hero-messages) + 2. [Thumbnail](#thumbnail-messages) + 3. [Receipt](#receipt-messages) +6. [Button types](#button-types) +7. [Handling errors](#handling-errors) + + +## Intro + +Skype Template Message builder allows you to generate more complex messages for Skype Messenger without writing JSON files manually. + +To use it, just require `skypeTemplate` function from _Claudia Bot Builder_: + +```javascript +const skypeTemplate = require('claudia-bot-builder').skypeTemplate; +``` + +`skypeTemplate` exports an object that contains multiple classes that allows you to generate different types of structured messages for Telegram: + +- Photo +- Carousel + +Carousel class gives you ability to add Hero, Thumbnail and Receipt messages. See more here: https://docs.botframework.com/en-us/skype/getting-started/#cards-and-buttons + +## Text messages + +If you simply want to answer with the text you can just return text. + + + +## Emoticon messages + +Skype doesn't have specific API method for sending emoticons. If you want to send emoticon, just retun a string with emoticon shortcut. You can find list of emoticons and shortcuts here: https://support.skype.com/en/faq/FA12330/what-is-the-full-list-of-emoticons + + + +## Photo messages + +Photo attachment allows you to send images. + +### API + +`Photo` (class) - Class that allows you to build a photo messages. + +_Arguments:_ + +- photo, base64 string URL (required) - a base64 encoded image. + +### Methods + +| Method | Required | Arguments | Returns | Description | +| ------------------------ | -------- | ---------------------------------------- | --------------------------------- | ---------------------------------------- | +| get | Yes | No arguments | Formatted JSON to pass as a reply | Get method is required and it returns a formatted JSON that is ready to be passed as a response to Skype Messenger | + +### Example + +```javascript +const botBuilder = require('claudia-bot-builder'); +const skypeTemplate = botBuilder.skypeTemplate; + +module.exports = botBuilder(message => { + if (message.type === 'skype') + return new skypeTemplate.Photo('data:image/png;base64,...').get(); +}); +``` + + + +## Carousel messages + +Carousel class allows you to send cards and buttons. + +### API + +`Carousel` (class) - Class that allows you to build carousel with Skype cards and buttons. + +_Arguments:_ + +- summary, string (optional) - a caption summary. +- text, integer (optional) - an optional text. + +### Methods + +| Method | Required | Arguments | Returns | Description | +| ------------------------ | -------- | ---------------------------------------- | --------------------------------- | ---------------------------------------- | +| addHero | No | See [Hero](#hero-messages) | `this` for chaining | Hero card | +| addThumbnail | No | See [Thumbnail](#thumbnail-messages) | `this` for chaining | Thumbnail card | +| addReceipt | No | See [Receipt](#receipt-messages) section | `this` for chaining | Receipt card | +| get | Yes | No arguments | Formatted JSON to pass as a reply | Get method is required and it returns a formatted JSON that is ready to be passed as a response to Telegram Messenger | + +### Example + +```javascript +const botBuilder = require('claudia-bot-builder'); +const skypeTemplate = botBuilder.skypeTemplate; + +module.exports = botBuilder(message => { + if (message.type === 'skype') + return new skypeTemplate.Carousel('summary', 'text') + .addHero(['http://lorempixel.com/400/200/']) + .addThumbnail(['http://lorempixel.com/400/200/']) + .addReceipt('$100') + .get(); +}); +``` + + + +## Hero messages + +Hero message requires Carousel class to be initialized. + +### API + +`addHero` (method) - Method that allows you to build Hero messages with optional images and buttons. + +_Arguments:_ + +- images, array (required) - an array of urls of images. + +### Methods + +| Method | Required | Arguments | Returns | Description | +| ------------------------ | -------- | ---------------------------------------- | --------------------------------- | ---------------------------------------- | +| addTitle | No | title (string, required, title for Hero) | `this` for chaining | Adds title on Hero message | +| addSubtitle | No | subtitle (string, required, subtitle for Hero) | `this` for chaining | Adds subtitle on Hero message | +| addText | No | text (string, required, text for Hero) | `this` for chaining | Adds text on Hero message | +| addButton | No | title (string, required, title of button), value (string, required, value of button), type (string, required, [Button types](#button-types)) | `this` for chaining | Adds button on Hero message | +| get | Yes | No arguments | Formatted JSON to pass as a reply | Get method is required and it returns a formatted JSON that is ready to be passed as a response to Telegram Messenger | + +### Example + +```javascript +const botBuilder = require('claudia-bot-builder'); +const skypeTemplate = botBuilder.skypeTemplate; + +module.exports = botBuilder(message => { + if (message.type === 'skype') + return new skypeTemplate.Carousel() + .addHero(['http://lorempixel.com/400/200/']) + .addTitle('New Hero') + .addSubtitle('Our new Hero') + .addText('Some description') + .addButton('Hi', 'hello', 'imBack') + .addButton('Other button', 'hello again', 'imBack') + .get(); +}); +``` + + + +## Thumbnail messages + +Thumbnail message requires Carousel class to be initialized. + +### API + +`addThumbnail` (method) - Method that allows you to build Thumbnail messages with optional images and buttons. + +_Arguments:_ + +- images, array (required) - an array of urls of images. + +### Methods + +| Method | Required | Arguments | Returns | Description | +| ------------------------ | -------- | ---------------------------------------- | --------------------------------- | ---------------------------------------- | +| addTitle | No | title (string, required, title for Thumbnail) | `this` for chaining | Adds title on Thumbnail message | +| addSubtitle | No | subtitle (string, required, subtitle for Thumbnail) | `this` for chaining | Adds subtitle on Thumbnail message | +| addText | No | text (string, required, text for Thumbnail) | `this` for chaining | Adds text on Thumbnail message | +| addButton | No | title (string, required, title of button), value (string, required, value of button), type (string, required, [Button types](#button-types)) | `this` for chaining | Adds button on Thumbnail message | +| get | Yes | No arguments | Formatted JSON to pass as a reply | Get method is required and it returns a formatted JSON that is ready to be passed as a response to Telegram Messenger | + +### Example + +```javascript +const botBuilder = require('claudia-bot-builder'); +const skypeTemplate = botBuilder.skypeTemplate; + +module.exports = botBuilder(message => { + if (message.type === 'skype') + return new skypeTemplate.Carousel() + .addThumbnail(['http://lorempixel.com/400/200/']) + .addTitle('New Thumbnail') + .addSubtitle('Our new Thumbnail') + .addText('Some description') + .addButton('Hi', 'hello', 'imBack') + .addButton('Other button', 'hello again', 'imBack') + .get(); +}); +``` + + + +## Receipt messages + +Receipt message requires Carousel class to be initialized. + +### API + +`addReceipt` (method) - Method that allows you to build Receipt messages with optional items and facts. + +_Arguments:_ + +- total, string (required) - total value. +- tax, string (required) - tax value. +- vat, string (required) - vat value. + +### Methods + +| Method | Required | Arguments | Returns | Description | +| ------------------------ | -------- | ---------------------------------------- | --------------------------------- | ---------------------------------------- | +| addTitle | No | title (string, required, title for Receipt) | `this` for chaining | Adds title on Receipt message | +| addSubtitle | No | subtitle (string, required, subtitle for Receipt) | `this` for chaining | Adds subtitle on Receipt message | +| addText | No | text (string, required, text for Receipt) | `this` for chaining | Adds text on Receipt message | +| addFact | No | key (string, required, key for Fact), value (string, required, value for Fact) | `this` for chaining | Adds fact on Receipt message | +| addItem | No | title (string, optional), subtitle (string, optional), text (string, optional), price (string, optional), quantity (string, optional), image (string, optional), | `this` for chaining | Adds item to Receipt message | +| addButton | No | title (string, required, title of button), value (string, required, value of button), type (string, required, [Button types](#button-types)) | `this` for chaining | Adds button on Receipt message | +| get | Yes | No arguments | Formatted JSON to pass as a reply | Get method is required and it returns a formatted JSON that is ready to be passed as a response to Telegram Messenger | + +### Example + +```javascript +const botBuilder = require('claudia-bot-builder'); +const skypeTemplate = botBuilder.skypeTemplate; + +module.exports = botBuilder(message => { + if (message.type === 'skype') + return new skypeTemplate.Carousel() + .addReceipt('100') + .addTitle('New Thumbnail') + .addSubtitle('Our new Thumbnail') + .addText('Some description') + .addFact('factKey', 'I am fact') + .addItem('Some item', 'I am some item', 'My description', '20', '5', 'http://lorempixel.com/400/200/') + .addButton('Hi', 'hello', 'imBack') + .addButton('Other button', 'hello again', 'imBack') + .get(); +}); +``` + + + +## Button types + +Skype buttons have specific types for the function they are supposed to do. Bellow is the table with the types and explanations: + +| Type | Explanation | +|------------------|-----------------------------------------------------------------------| +| openUrl | Open given url in the built-in browser. | +| imBack | Post message to bot, so all other participants will see that was posted to the bot and who posted this. | +| postBack | Post message to bot privately, so other participants inside conversation will not see that was posted. | +| playAudio | Playback audio container referenced by url. | +| playVideo | Playback video container referenced by url. | +| showImage | Show image referenced by url. | +| downloadFile | Download file referenced by url. | +| signin | Signin button. | + + + +## Handling errors + +Skype Template Message builder checks if messages you are generating are following Skype guidelines and limits, in case they are not an error will be thrown. + +_Example:_ + +Calling `new telegramTemplate.Carousel().addHero('imageUrl')` where's `image` passed as string instead of array will throw `Images should be sent as array for the Skype Hero template` error. + +All errors that Claudia bot builder's skypeTemplate library is throwing can be found [in the source code](../lib/skype/format-message.js). + +Errors will be logged in Cloud Watch log for your bot. diff --git a/lib/bot-builder.js b/lib/bot-builder.js index 6164133..15d9af8 100644 --- a/lib/bot-builder.js +++ b/lib/bot-builder.js @@ -14,6 +14,7 @@ const fbTemplate = require('./facebook/format-message'); const slackTemplate = require('./slack/format-message'); const telegramTemplate = require('./telegram/format-message'); const viberTemplate = require('./viber/format-message'); +const skypeTemplate = require('./skype/format-message'); const slackDelayedReply = require('./slack/delayed-reply'); let logError = function (err) { @@ -69,4 +70,5 @@ module.exports.fbTemplate = fbTemplate; module.exports.slackTemplate = slackTemplate; module.exports.telegramTemplate = telegramTemplate; module.exports.viberTemplate = viberTemplate; +module.exports.skypeTemplate = skypeTemplate; module.exports.slackDelayedReply = slackDelayedReply; diff --git a/lib/skype/format-message.js b/lib/skype/format-message.js new file mode 100644 index 0000000..2ea8f69 --- /dev/null +++ b/lib/skype/format-message.js @@ -0,0 +1,203 @@ +'use strict'; + +class SkypeMessage { + constructor() { + this.template = {}; + this.template.attachments = []; + } + + get() { + return this.template; + } +} + +class Photo extends SkypeMessage { + constructor(base64Photo) { + super(); + if (!base64Photo || typeof base64Photo !== 'string') + throw new Error('Photo is required for the Skype Photo template'); + + this.template = { + type: 'message/image', + attachments: [{ + contentUrl: base64Photo + }] + }; + } +} + +class Carousel extends SkypeMessage { + constructor(summary, text) { + super(); + + this.template = { + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: summary || '', + text: text || '', + attachments: [] + }; + + return this; + } + + getCurrentAttachment() { + let current = this.template.attachments.length - 1; + + if (current < 0) { + throw new Error('You need to add attachment to Carousel'); + } + + return current; + } + + addHero(images) { + if(images && !Array.isArray(images)) { + throw new Error('Images should be sent as array for the Skype Hero template'); + } + + this.template.attachments.push({ + contentType: 'application/vnd.microsoft.card.hero', + content: { + title: '', + subtitle: '', + text: '', + images: images ? images.map(image => ({url: image, alt: ''})) : [], + buttons: [] + } + }); + + return this; + } + + addThumbnail(images) { + if(images && !Array.isArray(images)) { + throw new Error('Images should be sent as array for the Skype Thumbnail template'); + } + + this.template.attachments.push({ + contentType: 'application/vnd.microsoft.card.thumbnail', + content: { + title: '', + subtitle: '', + text: '', + images: images ? images.map(image => ({url: image, alt: ''})) : [], + buttons: [] + } + }); + + return this; + } + + addReceipt(total, tax, vat) { + this.template.attachments.push({ + contentType: 'application/vnd.microsoft.card.receipt', + content: { + title: '', + subtitle: '', + text: '', + total: total || '', + tax: tax || '', + vat: vat || '', + items: [], + facts: [], + buttons: [] + } + }); + + return this; + } + + addFact(key, value) { + let currentAttachment = this.getCurrentAttachment(); + + this.template.attachments[currentAttachment].content.facts.push({ + key: key || '', + value: value || '' + }); + + return this; + } + + addItem(title, subtitle, text, price, quantity, image) { + let currentAttachment = this.getCurrentAttachment(); + + this.template.attachments[currentAttachment].content.items.push({ + title: title || '', + subtitle: subtitle || '', + text: text || '', + price: price || '', + quantity: quantity || '', + image: { + url: image || '' + } + }); + + return this; + } + + addTitle(title) { + let currentAttachment = this.getCurrentAttachment(); + + if (!title || typeof title !== 'string') + throw new Error('Title needs to be a string for Skype addTitle method'); + + this.template.attachments[currentAttachment].content.title = title; + + return this; + } + + addSubtitle(subtitle) { + let currentAttachment = this.getCurrentAttachment(); + + if (!subtitle || typeof subtitle !== 'string') + throw new Error('Subtitle needs to be a string for Skype addSubtitle method'); + + this.template.attachments[currentAttachment].content.subtitle = subtitle; + + return this; + } + + addText(text) { + let currentAttachment = this.getCurrentAttachment(); + + if (!text || typeof text !== 'string') + throw new Error('Text needs to be a string for Skype addText method'); + + this.template.attachments[currentAttachment].content.text = text; + + return this; + } + + addButton(title, value, type) { + let currentAttachment = this.getCurrentAttachment(); + + if (!title || typeof title !== 'string') + throw new Error('Title needs to be a string for Skype addButton method'); + + if (!value || typeof value !== 'string') + throw new Error('Value needs to be a string for Skype addButton method'); + + if (!type || typeof type !== 'string') + throw new Error('Type needs to be a string for Skype addButton method'); + + let validTypes = ['openUrl', 'imBack', 'postBack', 'playAudio', 'playVideo', 'showImage', 'downloadFile', 'signin']; + if (validTypes.indexOf(type) == -1) + throw new Error('Type needs to be a valid type string for Skype addButton method'); + + this.template.attachments[currentAttachment].content.buttons.push({ + type: type, + title: title, + value: value + }); + + return this; + } +} + +//TODO: investigate how to send Hero, Thumbnail and Receipt without carousel + +module.exports = { + Photo: Photo, + Carousel: Carousel +}; diff --git a/spec/skype/skype-format-message-spec.js b/spec/skype/skype-format-message-spec.js new file mode 100644 index 0000000..e05a061 --- /dev/null +++ b/spec/skype/skype-format-message-spec.js @@ -0,0 +1,298 @@ +/*global describe, it, expect, require */ +'use strict'; + +const formatMessage = require('../../lib/skype/format-message'); + +describe('Skype format message', () => { + it('should export an object', () => { + expect(typeof formatMessage).toBe('object'); + }); + + describe('Photo', () => { + it('should be a class', () => { + const message = new formatMessage.Photo('foo'); + expect(typeof formatMessage.Photo).toBe('function'); + expect(message instanceof formatMessage.Photo).toBeTruthy(); + }); + + it('should throw an error if photo url is not provided', () => { + expect(() => new formatMessage.Photo()).toThrowError('Photo is required for the Skype Photo template'); + }); + + it('should generate a valid Skype template object', () => { + const message = new formatMessage.Photo('base_64_string').get(); + expect(message).toEqual({ + type: 'message/image', + attachments: [ + { + contentUrl: 'base_64_string' + } + ] + }); + }); + }); + + describe('Carousel', () => { + it('should be a class', () => { + const message = new formatMessage.Photo('foo'); + expect(typeof formatMessage.Photo).toBe('function'); + expect(message instanceof formatMessage.Photo).toBeTruthy(); + }); + + it('should generate a valid Carousel template object', () => { + const message = new formatMessage.Carousel('summary', 'text').get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [] + }); + }); + + it('should throw error if addHero is called without images array', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero('image') + .get()).toThrowError('Images should be sent as array for the Skype Hero template'); + }); + + it('should generate a valid Carousel template object with Hero', () => { + const message = new formatMessage.Carousel('summary', 'text') + .addHero(['image']) + .addTitle('title') + .addSubtitle('subtitle') + .addText('text') + .get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [{ + contentType: 'application/vnd.microsoft.card.hero', + content: { + title: 'title', + subtitle: 'subtitle', + text: 'text', + images: [{url: 'image', alt: ''}], + buttons: [] + } + }] + }); + }); + + it('should throw error if addButton is called without attachment', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addButton('title', 'test', 'imBack') + .get()).toThrowError('You need to add attachment to Carousel'); + }); + + it('should throw error if addButton is called without title', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addButton() + .get()).toThrowError('Title needs to be a string for Skype addButton method'); + }); + + it('should throw error if addButton is called without value', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addButton('title', '', 'imBack') + .get()).toThrowError('Value needs to be a string for Skype addButton method'); + }); + + it('should throw error if addButton is called without type', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addButton('title', 'value') + .get()).toThrowError('Type needs to be a string for Skype addButton method'); + }); + + it('should throw error if addButton is called with strange type', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addButton('title', 'value', 'someType') + .get()).toThrowError('Type needs to be a valid type string for Skype addButton method'); + }); + + it('should generate a valid Carousel template object with Hero with Button', () => { + const message = new formatMessage.Carousel('summary', 'text') + .addHero() + .addButton('title', 'value', 'imBack') + .get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [{ + contentType: 'application/vnd.microsoft.card.hero', + content: { + title: '', + subtitle: '', + text: '', + images: [], + buttons: [{ + type: 'imBack', + title: 'title', + value: 'value' + }] + } + }] + }); + }); + + it('should throw error if addThumbnail is called without images array', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addThumbnail('title', 'subtitle', 'text', 'image') + .get()).toThrowError('Images should be sent as array for the Skype Thumbnail template'); + }); + + it('should generate a valid Carousel template object with Thumbnail', () => { + const message = new formatMessage.Carousel('summary', 'text') + .addThumbnail(['image']) + .addTitle('title') + .addSubtitle('subtitle') + .addText('text') + .get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [{ + contentType: 'application/vnd.microsoft.card.thumbnail', + content: { + title: 'title', + subtitle: 'subtitle', + text: 'text', + images: [{url: 'image', alt: ''}], + buttons: [] + } + }] + }); + }); + + it('should generate a valid Carousel template object with Receipt', () => { + const message = new formatMessage.Carousel('summary', 'text') + .addReceipt('total', 'tax', 'vat') + .addTitle('title') + .addSubtitle('subtitle') + .addText('text') + .get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [{ + contentType: 'application/vnd.microsoft.card.receipt', + content: { + title: 'title', + subtitle: 'subtitle', + text: 'text', + total: 'total', + tax: 'tax', + vat: 'vat', + items: [], + facts: [], + buttons: [] + } + }] + }); + }); + + it('should generate a valid Carousel template object with Receipt with Item', () => { + const message = new formatMessage.Carousel('summary', 'text') + .addReceipt('total', 'tax', 'vat') + .addTitle('title') + .addSubtitle('subtitle') + .addText('text') + .addItem('title', 'subtitle', 'text', 'price', 'quantity', 'image') + .get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [{ + contentType: 'application/vnd.microsoft.card.receipt', + content: { + title: 'title', + subtitle: 'subtitle', + text: 'text', + total: 'total', + tax: 'tax', + vat: 'vat', + items: [{ + title: 'title', + subtitle: 'subtitle', + text: 'text', + price: 'price', + quantity: 'quantity', + image: { + url: 'image' + } + }], + facts: [], + buttons: [] + } + }] + }); + }); + + it('should generate a valid Carousel template object with Receipt with Fact', () => { + const message = new formatMessage.Carousel('summary', 'text') + .addReceipt('total', 'tax', 'vat') + .addTitle('title') + .addSubtitle('subtitle') + .addText('text') + .addFact('key', 'value') + .get(); + expect(message).toEqual({ + type: 'message/card.carousel', + attachmentLayout: 'carousel', + summary: 'summary', + text: 'text', + attachments: [{ + contentType: 'application/vnd.microsoft.card.receipt', + content: { + title: 'title', + subtitle: 'subtitle', + text: 'text', + total: 'total', + tax: 'tax', + vat: 'vat', + items: [], + facts: [{ + key: 'key', + value: 'value' + }], + buttons: [] + } + }] + }); + }); + + it('should throw error if addTitle is called without title', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addTitle() + .get()).toThrowError('Title needs to be a string for Skype addTitle method'); + }); + + it('should throw error if addSubtitle is called without subtitle', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addSubtitle() + .get()).toThrowError('Subtitle needs to be a string for Skype addSubtitle method'); + }); + + it('should throw error if addText is called without text', () => { + expect(() => new formatMessage.Carousel('summary', 'text') + .addHero() + .addText() + .get()).toThrowError('Text needs to be a string for Skype addText method'); + }); + }); +});