From 6c7c07491bc5b4e379dba1a7bc249ebeb193e0f4 Mon Sep 17 00:00:00 2001 From: Victor Algaze Date: Sun, 10 Oct 2021 08:48:55 -0700 Subject: [PATCH] Cards, types, other util & fixes --- README.md | 50 ++----- docs/README.md | 23 +++ docs/how-to.md | 10 +- docs/util.md | 156 ++++++++++++++++++++ package.json | 1 + settings/namegame.ts | 10 +- src/cards.ts | 327 ++++++++++++++++++++++++++++++++++++++++++ src/easycard.ts | 5 + src/framework.ts | 7 + src/helpers.ts | 60 ++++++++ src/index.ts | 6 +- src/speedybot.ts | 89 ++++++------ test/easycard.test.ts | 82 ----------- tsconfig.json | 1 + 14 files changed, 653 insertions(+), 174 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/util.md create mode 100644 src/cards.ts delete mode 100644 test/easycard.test.ts diff --git a/README.md b/README.md index ce6d9f8..ff8e7b9 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Speedybot is a "toolkit" to take you from zero to a non-trivial bot as quickly a Speedybot instruments on top of the incredibly useful **[webex-node-bot-framework](https://github.com/WebexSamples/webex-node-bot-framework)** and steps through the fastest path to a working bot and provides some convenience features +Even if you don't use all of speedybot's features, if nothing else there are several helper utillties that are useful for crafting a rich conversation agent-- more details **[here](https://github.com/valgaze/speedybot/blob/master/docs/util.md)** Speedybot has one required dependency ## Adding a new chat handler @@ -73,6 +74,7 @@ There are a few "special" keywords you can use to "listen" to special events: ex. Tell the bot "sendcard" to get a card, type into the card & tap submit, catch submission using *<@submit>* and echo back to user ```ts +import { Card } from 'speedybot' export default [{ keyword: '<@submit>', handler(bot, trigger) { @@ -84,48 +86,14 @@ export default [{ keyword: 'sendcard', handler(bot, trigger) { bot.say('One card on the way...') - // Adapative Card: https://developer.webex.com/docs/api/guides/cards - const cardPayload = { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [{ - "type": "TextBlock", - "size": "Medium", - "weight": "Bolder", - "text": "System is πŸ‘" - }, { - "type": "RichTextBlock", - "inlines": [{ - "type": "TextRun", - "text": "If you see this card, everything is working" - }] - }, { - "type": "Image", - "url": "https://i.imgur.com/SW78JRd.jpg", - "horizontalAlignment": "Center", - "size": "large" - }, { - "type": "Input.Text", - "id": "inputData", - "placeholder": "What's on your mind?" - }], - "actions": [{ - "type": "Action.OpenUrl", - "title": "Take a moment to celebrate", - "url": "https://www.youtube.com/watch?v=3GwjfUFyY6M", - "style": "positive" - }, { - "type": "Action.Submit", - "title": "Submit", - "data": { - "cardType": "inputForm" - } - }] - } - - bot.sendCard(cardPayload, 'Your client does not currently support Adaptive Cards') + const myCard = new SpeedyCard().setTitle('System is πŸ‘') + .setSubtitle('If you see this card, everything is working') + .setImage('https://i.imgur.com/SW78JRd.jpg') + .setInput(`What's on your mind?`) + .setUrl('https://www.youtube.com/watch?v=3GwjfUFyY6M', 'Take a moment to celebrate') + .setTable([[`Bot's Date`, new Date().toDateString()], ["Bot's Uptime", `${String(process.uptime())}s`]]) + bot.sendCard(myCard.render(), 'Your client does not currently support Adaptive Cards') }, helpText: 'Sends an Adaptive Card with an input field to the user' } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..23407c9 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,23 @@ +## Speedybot + +- **[Various Helpers (cards, add variation, templating, storage, etc)](./util.md)** + +- **[Common Patterns & How-To](./how-to.md)** + +### Bot Token + +- Create new bot: https://developer.webex.com/my-apps/new/bot + +- Get an existing bot's token (tap "regenerate"): https://developer.webex.com/my-apps + +### Useful reading + +- https://developer.webex.com/blog/from-zero-to-webex-teams-chatbot-in-15-minutes + +- https://developer.webex.com/blog/introducing-the-webex-teams-bot-framework-for-node-js + +- https://github.com/WebexSamples/webex-card-school/blob/master/README.md + +- https://developer.webex.com/blog/five-tips-for-well-behaved-webex-bots + +- https://github.com/WebexSamples/webex-node-bot-framework/blob/master/docs/buttons-and-cards-example.md diff --git a/docs/how-to.md b/docs/how-to.md index da2b1c1..f5f5f5d 100644 --- a/docs/how-to.md +++ b/docs/how-to.md @@ -19,7 +19,11 @@ There are a few "special" keyword words you can use which have a special meaning - *<@webhook>*: Put your webhook handlers alongside your handlers, see the **["Handling Webhooks"](#handling-webhooks)** section below for an example or video instruction here: https://share.descript.com/view/bnyupJvNJcx -ex. + + + + +ex. Kitchen sink handler list ```ts export default handlers = [ @@ -319,8 +323,8 @@ Ex. Process incoming webhooks that post to your agent: ], } - this.send({toPersonEmail: 'valgaze+botexperiments@gmail.com', text: msg}) - this.sendCardToPerson('valgaze+botexperiments@gmail.com', cardJson) + this.send({toPersonEmail: 'joe@joe.com', text: msg}) + this.sendCardToPerson('joe@joe.com', cardJson) res.send('Thanks') // optional, in case server needs acknowledgement } diff --git a/docs/util.md b/docs/util.md new file mode 100644 index 0000000..4440757 --- /dev/null +++ b/docs/util.md @@ -0,0 +1,156 @@ +## Utilities and Helper Functions + +## SpeedyCard + +- Getting started with AdaptiveCards (https://developer.webex.com/docs/api/guides/cards) can be a bit cumbersome and error-prone + +- SpeedyCard is a limited subset of AdaptiveCards with basic features with a focus on user interaction & simplicity (title, text, input box, menu-select, no "collapsable" sections, etc) + +- Inspired a bit by SwiftUI: https://developer.apple.com/xcode/swiftui/ + +- Make sure you call ```.render()``` and use the ```.sendCard``` method + +
Before/After + +## Before + +```ts +const cardJson = { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [{ + "type": "TextBlock", + "text": "System is πŸ‘", + "weight": "Bolder", + "size": "Large", + "wrap": true + }, { + "type": "TextBlock", + "text": "If you see this card, everything is working", + "size": "Small", + "isSubtle": true, + "wrap": true, + "weight": "Lighter" + }, { + "type": "Image", + "url": "https://i.imgur.com/SW78JRd.jpg", + "horizontalAlignment": "Center", + "size": "Large" + }, { + "type": "Input.Text", + "placeholder": "What's on your mind?", + "id": "inputData" + }], + "actions": [{ + "type": "Action.Submit", + "title": "Submit", + "data": { + "cardType": "inputForm" + } + }, { + "type": "Action.OpenUrl", + "title": "Take a moment to celebrate", + "url": "https://www.youtube.com/watch?v=3GwjfUFyY6M" + }] +} + +bot.sendCard(cardJson, 'Your client does not currently support Adaptive Cards') +``` + +## After + +```ts +const cardJson = new SpeedyCard().setTitle('System is πŸ‘', {st}) + .setSubtitle('If you see this card, everything is working') + .setImage('https://i.imgur.com/SW78JRd.jpg') + .setInput(`What's on your mind?`) + .setUrl('https://www.youtube.com/watch?v=3GwjfUFyY6M', 'Take a moment to celebrate') + +bot.sendCard(cardJson.render(), 'Your client does not currently support Adaptive Cards') + +``` + +
+ + +```ts +import { SpeedyCard } from 'speedybot' +export const handlers = [{ + keyword: ['go!'], + handler(bot, trigger) { + // Adapative Card: https://developer.webex.com/docs/api/guides/cards + const myCard = new SpeedyCard().setTitle('System is πŸ‘', {st}) + .setSubtitle('If you see this card, everything is working') + .setImage('https://i.imgur.com/SW78JRd.jpg') + .setInput(`What's on your mind?`) + .setUrl('https://www.youtube.com/watch?v=3GwjfUFyY6M', 'Take a moment to celebrate') + .setChoices(['Abraham Lincoln','Adlai Stevenson','Connie Rice', 'Monty'], {id:}) + .setTable([[`Bot's Time`, new Date().toTimeString()], ['Bot running duration', process.uptime()]]) + bot.sendCard(myCard.render(), 'Your client does not currently support Adaptive Cards') + + } + helpText: 'A demo handler invoked when the user types go!' +}, +{ + keyword: '<@submit>', + handler(bot, trigger) { + bot.say(`Submission received! You sent us ${JSON.stringify(trigger.attachmentAction.inputs)}`) + }, + helpText: 'Special handler that fires when data is submitted' +} + +``` + +## Storage + +- Lightweight helper for storage, scoped to user + +- Get operations don't error if none found, return null instead + +```ts +import { Storage } from 'speedybot' + +const handler = { + keyword: ['go!'], + async handler(bot, trigger) { + const val = await Storage.get('secret_code') + if (val) { + bot.say(`Your stored value is ${val}`) + } else { + const secretCode = Math.random().toString(36).slice(2) + bot.say(`No stored value detected, saving '${secretCode}' now...`) + await Storage.get('secret_code', secretCode) + } + } + helpText: 'A demo handler invoked when the user types go!' +} +``` + + +## fillTemplate & pickRandom + +- Variation in responses is a low-investment, high-payoff way to make conversation agents more appealing and less robotic + +- pickRandom does exactly what it sounds like-- randomly return an item from a list + +- fillTemplate will randomly pick an item from a list and replace ```$[template_name]``` with a specified value + +```ts +import { fillTemplate } from 'speedybot' + +const handler = { + keyword: ['go!'], + async handler(bot, trigger) { + const phrases = ["Hey $[name], how's it going? Here is one $[flavor] ice cream", "Hiya $[name], here's your $[flavor]", "Here's one $[flavor] for ya, $[name]"] + const myTemplate = { + name: trigger.person.displayName, + flavor: 'mint' + } + + bot.say(fillTemplate(phrases, myTemplate)) + + } + helpText: 'A demo handler invoked when the user types go!' +} +``` \ No newline at end of file diff --git a/package.json b/package.json index c807fc0..daea584 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "1.0.6", "description": "Speedy & easy way to rapidly iterate with conversation bots", "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", "scripts": { "test": "npm run build && node_modules/.bin/tape dist/test/*.test.js", "patch": "npx np patch", diff --git a/settings/namegame.ts b/settings/namegame.ts index b8639aa..322a0f5 100644 --- a/settings/namegame.ts +++ b/settings/namegame.ts @@ -1,5 +1,5 @@ // See here: https://www.youtube.com/watch?v=NeF7jqf0GU4 -import { pickRandom, easyCard, Trigger, BotInst } from './../src' +import { pickRandom, Trigger, BotInst, SpeedyCard} from './../src' export default { keyword: ['namegame', 'namegame:start'], handler(bot: BotInst, trigger: Trigger) { @@ -16,14 +16,18 @@ export default { ex. namegame Lincoln 🎢🎢🎸🎢🎢 ` - return bot.sendCard(easyCard({ title: 'The Name Game by Shirley Ellis', text, url: 'https://www.youtube.com/watch?v=NeF7jqf0GU4', buttonLabel: 'Boogie!', image: 'https://i3.ytimg.com/vi/NeF7jqf0GU4/hqdefault.jpg' }), 'The Name Game by Shirley Ellis: https://www.youtube.com/watch?v=NeF7jqf0GU4') + + const myCard = new SpeedyCard().setTitle('The Name Game by Shirley Ellis').setSubtitle(text).setUrl('https://www.youtube.com/watch?v=NeF7jqf0GU4').setImage('https://i3.ytimg.com/vi/NeF7jqf0GU4/hqdefault.jpg') + return bot.sendCard(myCard.render(), 'It appears your client does not support adaptive cards') } const firstName = name ? name : trigger.person.firstName; const res = lyricsGenerator(firstName) const warmup = ['Alright,', 'Here we go', 'Ready?', 'Deep breath...'] const output = `${pickRandom(warmup)} ${res}` - bot.sendCard(easyCard({ title: 'The Name Game by Shirley Ellis', text: output, url: 'https://www.youtube.com/watch?v=NeF7jqf0GU4', buttonLabel: 'Go!', image: 'https://i3.ytimg.com/vi/NeF7jqf0GU4/hqdefault.jpg' }), 'The Name Game by Shirley Ellis: https://www.youtube.com/watch?v=NeF7jqf0GU4') + + const myCard = new SpeedyCard().setTitle('The Name Game by Shirley Ellis').setSubtitle(output).setUrl('https://www.youtube.com/watch?v=NeF7jqf0GU4').setImage('https://i3.ytimg.com/vi/NeF7jqf0GU4/hqdefault.jpg') + bot.sendCard(myCard.render(), 'The Name Game by Shirley Ellis: https://www.youtube.com/watch?v=NeF7jqf0GU4') if (!name) { bot.say('Psst, try adding a name, like this: @botname namegame Shirley') } diff --git a/src/cards.ts b/src/cards.ts new file mode 100644 index 0000000..d701d45 --- /dev/null +++ b/src/cards.ts @@ -0,0 +1,327 @@ + +export interface BaseConfig { + title?: string; + titleConfig?: Partial; + choices?: string[]; + buttons?: string[]; +} + +export interface BaseOpts { + horizontalAlignment?: "Left" | "Center" | "Right"; + size?: "Small" | "Default" | "Medium" | "Large" | "ExtraLarge"; +} + +export interface ChoiceOption { + title: string; + value: string; +} + +export interface ChoiceBlock { + type?: string; // "Input.ChoiceSet" + id?: string; + value?: string; + isMultiSelect?: boolean; + isVisible?: boolean; + choices?: ChoiceOption[] +} + +export interface TextBlock extends BaseOpts { + type: "TextBlock"; + text: string; + color?: "Default" | "Dark" | "Light" | "Accent" | "Good" | "Warning" | "Attention"; + fontType?: string; + isSubtle?: boolean; + weight: "Lighter" | "Default" | "Bolder"; + wrap?: boolean; +} + +export interface ImageBlock extends BaseOpts { + type:"Image"; + url: string; +} + +export interface LinkButton { + type: "Action.OpenUrl"; + title: string; + url: string; + style?: "positive" | "destructive"; +} + +export interface inputConfig { + id: string; + placeholder?: string; +} + + +/** + * SpeedyCard + * Work in progress + * - zero-knowledge, easy declarative way to construct + * "rich" (ie interactive adpative cards) + * + * - Chain methods together, kinda like SwiftUI's syntax: https://developer.apple.com/xcode/swiftui/ + * + * ```ts + import { SpeedyCard } from 'speedybot' + + const cardPayload = new SpeedyCard().setTitle('System is πŸ‘') + .setSubtitle('If you see this card, everything is working') + .setImage('https://i.imgur.com/SW78JRd.jpg') + .setInput(`What's on your mind?`) + .setUrl(pickRandom(['https://www.youtube.com/watch?v=3GwjfUFyY6M', 'https://www.youtube.com/watch?v=d-diB65scQU']), 'Take a moment to celebrate') + .setTable([[`Bot's Date`, new Date().toDateString()], ["Bot's Uptime", `${String(process.uptime()).substring(0, 25)}s`]]) + + bot.sendCard(cardPayload.render(), 'Your client doesnt appear to support adaptive cards') + * ``` + */ +export class SpeedyCard { + public title = '' + public subtitle = '' + public titleConfig: Partial = {} + public subTitleConfig: Partial = {} + public choices: ChoiceOption[] = [] + public choiceConfig: Partial = {} + public image: string = ''; + public imageConfig: BaseOpts = {}; + public buttonLabel = 'Submit' + public inputPlaceholder = '' + public inputConfig: inputConfig = { + id: 'inputData', + } + public url = '' + public urlLabel = 'Go' + public tableData: string[][] = [] + + public json:EasyCardSpec = { + "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + "type": "AdaptiveCard", + "version": "1.0", + "body": [] + } + + constructor() {} + + setTitle(title:string, config?: Partial) { + this.title = title + if (config) { + this.titleConfig = config + } + return this + } + + setSubtitle(subtitle: string, config?: Partial) { + this.subtitle = subtitle + if (config) { + this.subTitleConfig = config + } + return this + } + + setChoices(choices: string[], config?:ChoiceBlock) { + this.choices = choices.map((choice: string, idx) => { + return { + title: choice, + value: String(idx) + } + }) + if (config) { + this.choiceConfig = config + } + return this + } + + setImage(url: string, imageConfig?) { + this.image = url + if (imageConfig) { + this.imageConfig = imageConfig + } + return this + } + + setButtonLabel(label: string) { + this.buttonLabel = label + return this + } + + setInput(placeholder, config?) { + this.inputPlaceholder = placeholder + if (config) { + this.inputConfig = config + } + return this + } + + setUrl(url: string, label='Go') { + this.urlLabel = label + this.url = url + return this + } + + setTable(input: string[][]) { + let core = input + if (!Array.isArray(input) && typeof input === 'object') { + core = Object.entries(input) + } + this.tableData = core + return this + } + + render() { + let needsSubmit = false + if (this.title) { + const payload:TextBlock = { + type: 'TextBlock', + text: this.title, + weight: 'Bolder', + size: 'Large', + wrap: true, + ...this.titleConfig + } + this.json.body.push(payload) + } + + if (this.subtitle) { + const payload:TextBlock = { + type: 'TextBlock', + text: this.subtitle, + size: "Small", + isSubtle: true, + wrap:true, + weight: 'Lighter', + ...this.subTitleConfig + } + this.json.body.push(payload) + } + + if (this.tableData && this.tableData.length) { + const payload = { + "type": "ColumnSet", + "columns": [], + "spacing": "Padding", + "horizontalAlignment": "Center" + } + + const columnsData: { type: string, width: number, items: any[] }[] = [ + { + "type": "Column", + "width": 35, + "items": [] + }, + { + "type": "Column", + "width": 65, + "items": [] + } + ] + + const buildLabel = (label: string) => { + return { + "type": "TextBlock", + "text": label, + "weight": "Bolder", + "color": "Light", + "spacing": "Small" + } + } + const buildValue = (value: string) => { + return { + "type": "TextBlock", + "text": value, + "color": "Light", + "weight": "Lighter", + "spacing": "Small" + } + } + + this.tableData.forEach(([label, value], i) => { + columnsData[0].items.push(buildLabel(label)) + columnsData[1].items.push(buildValue(value)) + }) + + // @ts-ignore + payload.columns = columnsData + + this.json.body.push(payload) + } + + if (this.image) { + const payload: ImageBlock = { + type: "Image", + url: this.image, + horizontalAlignment: "Center", + size: "Large", + ...this.imageConfig + } + this.json.body.push(payload) + } + + if (this.choices.length) { + needsSubmit = true + const payload: ChoiceBlock = { + type: 'Input.ChoiceSet', + id: 'choiceSelect', + "value": "0", // Pick 1st one? + "isMultiSelect": false, + "isVisible": true, + choices: this.choices, + ...this.choiceConfig + } + this.json.body.push(payload) + } + + if (this.inputPlaceholder) { + needsSubmit = true + const payload = { + "type": "Input.Text", + placeholder: this.inputPlaceholder, + ...this.inputConfig, + } + this.json.body.push(payload) + } + + + if (needsSubmit) { + const payload = { + type: "Action.Submit", + title: this.buttonLabel, + "data": { + "cardType": "inputForm" + } + } + this.json.actions = [payload] + } + + if (this.url) { + const payload: LinkButton = { + type: "Action.OpenUrl", + title: this.urlLabel, + url: this.url, + } + if (this.json.actions) { + this.json.actions.push(payload) + } else { + this.json.actions = [payload] + } + } + return this.json + } + + renderFull() { + const cardData = this.render() + const fullPayload = { + "roomId": "__REPLACE__ME__", + "markdown": "Fallback text **here**", + "attachments": [cardData] + } + return fullPayload + } +} + +// todo: better types +export interface EasyCardSpec { + $schema: string; + type: string; + version: string; + body: any; + actions?: any; +} \ No newline at end of file diff --git a/src/easycard.ts b/src/easycard.ts index 72a301f..175c317 100644 --- a/src/easycard.ts +++ b/src/easycard.ts @@ -10,6 +10,11 @@ * */ + + + + +// legacy export interface EasyCardPayload { title?: string; url: string; diff --git a/src/framework.ts b/src/framework.ts index 3ee618b..44e2204 100644 --- a/src/framework.ts +++ b/src/framework.ts @@ -32,6 +32,7 @@ export interface FrameworkInst { setAuthorizer(func: any); boolean; clearAuthorizer(): void; on(eventName: string, handler: unknown): void; + onMessageCreated(payload: Message): void } export interface FrameworkOptions { @@ -244,6 +245,12 @@ export interface WebhookHandler { handler: AlertFunc; method?: ValidMethods; // default to post } + +export const passThru = (bot: BotInst, trigger: Trigger) => { + // HACK: pass the button-tap value through the handler system + return bot.framework.onMessageCreated(trigger.message) +} + /** * const alerter = { * keyword: '<@webhook>' diff --git a/src/helpers.ts b/src/helpers.ts index b975c0f..5343846 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,3 +1,4 @@ +import { Trigger } from './framework' import { loud } from './logger' /** * @param list @@ -111,3 +112,62 @@ See here for more details: https://github.com/valgaze/speedybot/blob/master/docs } return payload } + + +export const jsonSnippet = (payload) => { + const escaped = ` +\`\`\`json +${JSON.stringify(payload, null, 2)} +\`\`\`` + return { markdown: escaped } +} + +// Alias store/recall +export class Storage { + static async get(bot, key:string) { + let res = null + try { + res = await bot.recall(key) + } catch(e) { + + } + return res + } + + static async save(bot, key:string, val: any) { + return bot.store(key, val) + } + + static async delete(bot, key) { + return bot.forget(key) + } +} + + +export class Locker { + constructor(public state: T = {} as T) {} + + save(trigger:Trigger, key: string, value: unknown) { + const { personId } = trigger + if (!this.state[personId]) { + this.state[personId] = {} + } + this.state[personId][key] = value + } + + get(trigger:Trigger, key: string) { + const { personId } = trigger + return this.state[personId] ? (this.state[personId][key] || null) : null + } + + delete(trigger: Trigger, key: string) { + const { personId } = trigger + if (this.state[personId]) { + delete this.state[personId][key] + } + } + + snapShot() { + return JSON.parse(JSON.stringify(this.state)) + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index cee9679..da5e398 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,10 +2,10 @@ export { Speedybot, SpeedybotWebhook, Speedytunnel, Launch } from './speedybot' export { SpeedybotConfig } from './speedybot' // Types: framework -export { FrameworkInst, BotHandler,WebhookHandler, Message, ToMessage, BotInst, Trigger } from './framework' +export { FrameworkInst, BotHandler,WebhookHandler, Message, ToMessage, BotInst, Trigger, passThru } from './framework' export { bad, help, ascii_art, log, good, askQuestion, loud } from './logger' // helpers -export { fillTemplate, pickRandom } from './helpers' +export { fillTemplate, pickRandom, jsonSnippet, Storage, Locker } from './helpers' // make adaptive cards less painful w/ base templates -export { easyCard } from './easycard' +export { SpeedyCard } from './cards' export const placeholder = '__REPLACE__ME__' diff --git a/src/speedybot.ts b/src/speedybot.ts index 1a8fabb..c417398 100644 --- a/src/speedybot.ts +++ b/src/speedybot.ts @@ -1,6 +1,6 @@ import { FrameworkInst, BotHandler, ToMessage, BotInst, Trigger, WebhookHandler } from './framework' import { ValidatewebhookUrl, pickRandom } from './helpers' -import { placeholder, ascii_art } from './' +import { placeholder, ascii_art, SpeedyCard } from './' // TODO: make peer dependency import Botframework from 'webex-node-bot-framework' import BotWebhook from 'webex-node-bot-framework/webhook' @@ -24,7 +24,7 @@ export class Speedybot { // Chat/framework handler mappings Magickeywords = { - '<@help>': ['help'], + '<@help>': 'help', '<@catchall>': /(.*?)/, } @@ -203,46 +203,51 @@ export class Speedybot { bot.say(pickRandom(choices)) // Adapative Card: https://developer.webex.com/docs/api/guides/cards - // Card with n buttons, each emits "chip_action" with their label value - const cardPayload = { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [{ - "type": "TextBlock", - "size": "Medium", - "weight": "Bolder", - "text": "System is πŸ‘" - }, { - "type": "RichTextBlock", - "inlines": [{ - "type": "TextRun", - "text": "If you see this card, everything is working" - }] - }, { - "type": "Image", - "url": "https://i.imgur.com/SW78JRd.jpg", - "horizontalAlignment": "Center", - "size": "large" - }, { - "type": "Input.Text", - "id": "inputData", - "placeholder": "What's on your mind?" - }], - "actions": [{ - "type": "Action.OpenUrl", - "title": "Take a moment to celebrate", - "url": "https://www.youtube.com/watch?v=3GwjfUFyY6M", - "style": "positive" - }, { - "type": "Action.Submit", - "title": "Submit", - "data": { - "cardType": "inputForm" - } - }] - } - return bot.sendCard(cardPayload, 'Your client does not currently support Adaptive Cards :(') + const cardPayload = new SpeedyCard().setTitle('System is πŸ‘') + .setSubtitle('If you see this card, everything is working') + .setImage('https://i.imgur.com/SW78JRd.jpg') + .setInput(`What's on your mind?`) + .setUrl(pickRandom(['https://www.youtube.com/watch?v=3GwjfUFyY6M', 'https://www.youtube.com/watch?v=d-diB65scQU']), 'Take a moment to celebrate') + .setTable([[`Bot's Date`, new Date().toDateString()], ["Bot's Uptime", `${String(process.uptime()).substring(0, 25)}s`]]) + // const cardPayload = { + // "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", + // "type": "AdaptiveCard", + // "version": "1.0", + // "body": [{ + // "type": "TextBlock", + // "size": "Medium", + // "weight": "Bolder", + // "text": "System is πŸ‘" + // }, { + // "type": "RichTextBlock", + // "inlines": [{ + // "type": "TextRun", + // "text": "If you see this card, everything is working" + // }] + // }, { + // "type": "Image", + // "url": "https://i.imgur.com/SW78JRd.jpg", + // "horizontalAlignment": "Center", + // "size": "large" + // }, { + // "type": "Input.Text", + // "id": "inputData", + // "placeholder": "What's on your mind?" + // }], + // "actions": [{ + // "type": "Action.OpenUrl", + // "title": "Take a moment to celebrate", + // "url": "https://www.youtube.com/watch?v=3GwjfUFyY6M", + // "style": "positive" + // }, { + // "type": "Action.Submit", + // "title": "Submit", + // "data": { + // "cardType": "inputForm" + // } + // }] + // } + return bot.sendCard(cardPayload.render(), 'Your client does not currently support Adaptive Cards :(') }, helpText: 'Test the health of your bot. Otherwise there may be an issue with your tunnel, server, or network)' } diff --git a/test/easycard.test.ts b/test/easycard.test.ts deleted file mode 100644 index e6db782..0000000 --- a/test/easycard.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import test from "tape"; -import { easyCard } from './../src/easycard' - -test("setup", function (t) { - t.end(); -}); - -test("kitchen sink", (t) => { - const expected = { - "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", - "type": "AdaptiveCard", - "version": "1.0", - "body": [ - { - "type": "TextBlock", - "size": "Medium", - "weight": "Bolder", - "text": "card title" - }, - { - "type": "RichTextBlock", - "inlines": [ - { - "type": "TextRun", - "text": "text" - } - ] - }, - { - "type": "Image", - "url": "https://i.imgur.com/VQoXfHn.gif", - "horizontalAlignment": "Center", - "size": "large" - }, - { - "type": "Input.Text", - "id": "inputData", - "placeholder": "Send data" - } - ], - "actions": [ - { - "type": "Action.OpenUrl", - "title": "Button label", - "url": "https://i.imgur.com/VQoXfHn.gif", - "style": "positive" - }, - { - "type": "Action.Submit", - "title": "Submit", - "data": { - "cardType": "inputForm" - } - } - ] - } - - const actual = easyCard({ - title: 'card title', - url: 'https://i.imgur.com/VQoXfHn.gif', - text: 'text', - buttonLabel: 'Button label', - image: 'https://i.imgur.com/VQoXfHn.gif', - input: { - placeholder: 'Send data' - } - }) - t.deepEqual(actual, expected); - t.end(); -}); - - -test("add an image", (t) => { - const expected = [1,2,3,4] - const actual = [1,2,3,4] - t.deepEqual(actual, expected); - t.end(); -}); - -test("teardown", function (t) { - t.end(); -}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 4482849..2870ec7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "declaration": true, "resolveJsonModule": true, "module": "commonjs", "esModuleInterop": true,