From 7f01fa767b21497e5e73b64c5b1affb9bc44f6b0 Mon Sep 17 00:00:00 2001 From: Adrian Lopez Date: Wed, 7 Sep 2016 11:10:38 +0200 Subject: [PATCH 1/3] Change the adaptor to use the new Bot Builder SDK --- README.md | 39 ++++++----- package.json | 9 ++- src/skype.coffee | 172 +++++++++++++++++++++++++++++++---------------- 3 files changed, 142 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index e7f77f6..b71656e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # hubot-skype-bot -A Hubot adapter for the official [Skype Bots API (Preview)][skypebots]. +A Hubot adapter for the official [Microsoft Bot Framework (Preview)][botframework]. -This adapter relies on **Skype NodeJS SDK**. +This adapter relies on [**Bot Framework NodeJS SDK**][botframeworknodejs]. Refer to Skype Bots [documentation][] for more information. @@ -11,32 +11,36 @@ See [`src/skype.coffee`](src/skype.coffee) for full documentation. ## Getting Started -You need 3 pieces of _credentials_ before getting started with `hubot-skype-bot`: a Skype bot ID, a Microsoft application ID, and a secret associated with the application ID. +You need 2 pieces of _credentials_ before getting started with `hubot-skype-bot`: a Microsoft application ID, and a password associated with the application ID. ### 1. Create Skype Bot -To obtain the Skype bot ID, start by [creating a new Skype bot][createbot] (https://developer.microsoft.com/en-us/skype/bots/manage/Create). +To obtain a new bot, start by [registering a new one][createbot]. -Tick both _"Send and receive messages and content in 1:1 chat"_, and _"Send and receive messages and content in a group chat (limited preview for developer accounts only)"_. - -For the _"Messaging Webhook"_, set it to the URL that your bot will be hosted, and accessible from, followed by a `/skype/` path. For example, if you are using [`ngrok`][ngrok] to expose your locally hosted bot, you will be entering something like: `https://unique-id.ngrok.io/skype/` +For the _"Messaging Endpoint"_, set it to the URL (HTTPS) that your bot will be hosted, and accessible from, followed by a `/skype/` path. For example, if you are using [`ngrok`][ngrok] to expose your locally hosted bot, you will be entering something like: `https://unique-id.ngrok.io/skype/` During the creation process, you will be asked for a _Microsoft Application ID_. +After bot is created, in _Skype Channel_, click on _Edit_ and enable _Group messaging_. + +To start speaking with it click in the _Add to Skype_ button (in Linux, open [Skype Web](https://web.skype.com) before clicking the add button). + ### 2. Create Microsoft Application -There should be a link to the [Microsoft Application Registration Portal][appportal] (https://apps.dev.microsoft.com/). Once you create an application, you will be given an application ID, and a secret associated with the application ID. +There should be a link to the [Create Microsoft App ID and password][appportal]. Once you create an application, you will be given an application ID, and a secret associated with the application ID. ### 3. Set Environment Variables -You should now have the 3 aforementioned pieces of _credentials_. Expose them to your bot environment: +You should now have the 2 aforementioned pieces of _credentials_. Expose them to your bot environment: ```bash -export SKYPE_BOT_ID="BOT ID HERE" export MICROSOFT_APP_ID="APP ID HERE" -export MICROSOFT_APP_SECRET="APP SECRET HERE" +export MICROSOFT_APP_PASSWORD="APP PASSWORD HERE" ``` +One Hubot is running, click in _Test connection to your bot_ in [your bot page][botframeworkbots]. +This will send a POST to your endpoint that will be answered with a HTTP 100. + ## Installation via NPM @@ -55,13 +59,14 @@ Now, run Hubot with the `skype-bot` adapter: Variable | Default | Description --- | --- | --- -`MICROSOFT_APP_ID` | N/A | Your bot's unique ID (https://developer.microsoft.com/en-us/skype/bots/manage) -`MICROSOFT_APP_SECRET` | N/A | A Microsoft application ID to authenticate your bot (https://apps.dev.microsoft.com/) -`SKYPE_BOT_ID` | N/A | A Microsoft application secret associated with your application ID (https://apps.dev.microsoft.com/) +`MICROSOFT_APP_ID` | N/A | Your bot's unique ID (https://dev.botframework.com/bots) +`MICROSOFT_APP_PASSWORD` | N/A | A Microsoft application ID to authenticate your bot (https://apps.dev.microsoft.com/) -[skypebots]: https://developer.microsoft.com/skype/bots -[documentation]: https://developer.microsoft.com/en-us/skype/bots/docs -[createbot]: https://developer.microsoft.com/en-us/skype/bots/manage/Create +[botframework]: https://dev.botframework.com/ +[botframeworkbots]: https://dev.botframework.com/bots +[botframeworknodejs]: https://docs.botframework.com/en-us/node/builder/chat-reference/modules/_botbuilder_d_.html +[documentation]: https://docs.botframework.com/en-us/skype/getting-started +[createbot]: https://dev.botframework.com/bots/new [appportal]: https://apps.dev.microsoft.com/ [ngrok]: https://ngrok.com/ diff --git a/package.json b/package.json index 569e8e4..b4af1e6 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "hubot-skype-bot", - "description": "A Hubot adapter for the official Skype Bots API (Preview).", - "version": "1.0.1", + "description": "A Hubot adapter for the official Microsoft Bot Framework (Preview).", + "version": "2.0.0", "author": "Ian Lai ", + "contributors": [ + "Adrian Lopez " + ], "license": "MIT", "keywords": [ "hubot", @@ -21,7 +24,7 @@ "url": "https://github.com/ClaudeBot/hubot-skype-bot/issues" }, "dependencies": { - "skype-sdk": "https://devportalassets.azureedge.net/files/skype-sdk.tar.gz" + "botbuilder": "~3.2.2" }, "peerDependencies": {}, "main": "./src/skype.coffee", diff --git a/src/skype.coffee b/src/skype.coffee index 096a5b5..c3fcd0c 100644 --- a/src/skype.coffee +++ b/src/skype.coffee @@ -1,82 +1,138 @@ {Robot, Adapter, TextMessage} = require "hubot" -skype = require "skype-sdk"; +builder = require "botbuilder"; # Microsoft botframework +# Skype adaptator class Skype extends Adapter constructor: (@robot) -> + # @robot is hubot super @robot - @botService = null + # @bot is botframework + @bot = null + @intents = null + + # Env vars to configure the botframework @appID = process.env.MICROSOFT_APP_ID - @appSecret = process.env.MICROSOFT_APP_SECRET - @botID = process.env.SKYPE_BOT_ID - @apiURL = "https://apis.skype.com" - @apiTimeout = 15000 - @robot.logger.info "hubot-skype-bot: Adapter loaded." + @appPassword = process.env.MICROSOFT_APP_PASSWORD - _nameFromId: (userId) -> - parts = userId.split(":") - parts[parts.length - 1] + @robot.logger.info "hubot-skype-bot: Adapter loaded." - _createUser: (userId, roomId = false, displayName = "") -> + _createUser: (userId, address) -> user = @robot.brain.userForId(userId) - user.room = roomId if roomId - user.name = displayName - if displayName.length < 1 - user.name = @_nameFromId(userId) - @robot.logger.info("hubot-skype-bot: new user : ", user) + if typeof address != 'undefined' + user.address = address + @robot.logger.debug("hubot-skype-bot: new user : ", user) user - _processMsg: (msg) -> - retun unless msg.from? and msg.content? - user = @_createUser msg.from, msg.to - _msg = msg.content.trim() - - # Format for PMs - _msg = @robot.name + " " + _msg if msg.to is @botID - - message = new TextMessage user, _msg, msg.messageId - @receive(message) if message? - - _sendMsg: (context, msg) -> - # TODO: add, and test support for rooms ... - target = context.user.id - target = context.user.room if context.user.room isnt @botID - @botService.send(target, msg, true, (err) => - @robot.logger.error("hubot-skype-bot: error sending message : ", err) if err? - ) - + _sendMsg: (address, text) => + @robot.logger.debug "Bot msg: #{text}" + msg = new builder.Message() + msg.textFormat("plain") # By default is markdown + msg.address(address) + msg.text(text) + @bot.send msg, (err) => + if typeof err == 'undefined' + @robot.logger.error "Sending msg to Skype #{err}" + else + @robot.logger.debug "Msg to Skype sended correctly" + return + + # Function used by Hubot to answer send: (envelope, strings...) -> - @_sendMsg envelope, strings.join "\n" + @robot.logger.debug "Send" + @_sendMsg envelope.user.address, strings.join "\n" reply: (envelope, strings...) -> - @_sendMsg envelope, envelope.user.name + ": " + strings.join "\n #{envelope.user.name}: " + @robot.logger.debug "Reply" + @_sendMsg envelope.user.address, strings.join "\n" + # Pass the msg to Hubot, appending the bot name at the beggining + _processMsg: (msg) -> + user = @_createUser msg.user.id, msg.address + # Remove name. This is received by the bot when called from a group + # Append robot name at the beggining + text = @robot.name + " " + msg.text.replace /.*<\/at>\s+(.*)$/, "$1" + message = new TextMessage user, text, msg.address.id + # @receive pass the message to Hubot internals + @receive(message) if message? + + # Adapter start run: -> unless @appID @emit "error", new Error "You must configure the MICROSOFT_APP_ID environment variable." - unless @appSecret - @emit "error", new Error "You must configure the MICROSOFT_APP_SECRET environment variable." - unless @botID - @emit "error", new Error "You must configure the SKYPE_BOT_ID environment variable." - - @botID = "28:#{@botID}" - @botService = new skype.BotService - messaging: - botId: @botID, - serverUrl: @apiURL, - requestTimeout: @apiTimeout, - appId: @appID, - appSecret: @appSecret - - @robot.router.post "/skype/", skype.messagingHandler(@botService) - - @botService.on("message", (bot, data) => - @_processMsg data - ) + unless @appPassword + @emit "error", new Error "You must configure the MICROSOFT_APP_PASSWORD environment variable." - @botService.on("contactAdded", (bot, data) => - @_createUser data.from, false, data.fromDisplayName + @connector = new (builder.ChatConnector)( + appId: @appID + appPassword: @appPassword ) + # Creating bot of botframework + @bot = new (builder.UniversalBot)(@connector) + + # HTTP POST to /skype/ are passed to botframework (by default port 8080) + @robot.router.post "/skype/", @connector.listen() + + # Anything received by the bot is parsed by the defined intents + # If nothing is matched, pass the msg to Hubot + @intents = new (builder.IntentDialog) + @bot.dialog '/', @intents + + # Intents regex starts with .* to also match callings from groups, that appends ... + # The matches function needs a regexp for the first arguments, then an array of anonymous funcs + # Those anonymous functions receive session param, which we could use to answer, store values, etc. + # https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.session.html + @intents.matches /.*example$/i, [ + (session) => + session.send "This bot is a mixture between BotFramwork de Microsoft y Hubot. +If you write 'example', this message is shown.\n\n +If you write 'chat', an example dialog is started.\n\n +Otherwise, the message is passed to hubot (write for example 'ping').\n\n\n\n +In group chats, bot only will receive messages send to it. Eg.: @botname example" + return + ] + + # This example intent has two anonymous funcs. First one start a dialog (botframework function) with the user. + # Second one is executed after and receive the values written by the user + @intents.matches /.*chat$/i, [ + (session) => + session.beginDialog '/.*chat$/i' + return + (session, results) => + session.send "Dialog results #{results}" + return + ] + + # Si ninguno de los otros intents ha matcheado, entra en este, que pasa el mensaje a Hubot + # Esta seria la conexion entre los mensajes de Skype y Hubot + @intents.onDefault [ + (session, args, next) => + @robot.logger.debug "Msg from the user: #{session.message.text}" + @_processMsg session.message + return + ] + + # If user wants to exit from any dialog at any moment it can write "goodbye" + @bot.endConversationAction('goodbye', 'Closing dialog', { matches: /^goodbye/i }); + + # This dialog receives as first argument a name (to be called from intents) and an array of anonymous functions (as seen with intents) + # Normally you send answers and receive responses in the next func + @bot.dialog '/chat', [ + (session) -> + builder.Prompts.text session, "Tell me someting" + return + (session, results) -> + # This is to remove from the response of a user in case of group chats + resp = results.response.replace /.*<\/at>\s+(.*)$/, "$1" + + # We can use session.userData to store values + # Eg.: session.userData.channel_name = resp + + # To finish the dialog + session.endDialog() + return + ] + @robot.logger.info "hubot-skype-bot: Adapter running." @emit "connected" From 3584a0ec67653b4ebf71eca4dd3f15a4cf80a117 Mon Sep 17 00:00:00 2001 From: Adrian Lopez Date: Thu, 8 Sep 2016 09:44:30 +0200 Subject: [PATCH 2/3] Fix invalid dialog, regex. Answer to user text written --- src/skype.coffee | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/skype.coffee b/src/skype.coffee index c3fcd0c..56e2d09 100644 --- a/src/skype.coffee +++ b/src/skype.coffee @@ -82,7 +82,7 @@ class Skype extends Adapter # The matches function needs a regexp for the first arguments, then an array of anonymous funcs # Those anonymous functions receive session param, which we could use to answer, store values, etc. # https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.session.html - @intents.matches /.*example$/i, [ + @intents.matches /example$/i, [ (session) => session.send "This bot is a mixture between BotFramwork de Microsoft y Hubot. If you write 'example', this message is shown.\n\n @@ -94,12 +94,12 @@ In group chats, bot only will receive messages send to it. Eg.: @botname example # This example intent has two anonymous funcs. First one start a dialog (botframework function) with the user. # Second one is executed after and receive the values written by the user - @intents.matches /.*chat$/i, [ + @intents.matches /chat$/i, [ (session) => - session.beginDialog '/.*chat$/i' + session.beginDialog '/chat' return (session, results) => - session.send "Dialog results #{results}" + console.log "Dialog results: #{results}" return ] @@ -124,6 +124,7 @@ In group chats, bot only will receive messages send to it. Eg.: @botname example (session, results) -> # This is to remove from the response of a user in case of group chats resp = results.response.replace /.*<\/at>\s+(.*)$/, "$1" + session.send "You said: #{resp}" # We can use session.userData to store values # Eg.: session.userData.channel_name = resp From 510538d819e6ed599fcda8c2411455fd62c764b3 Mon Sep 17 00:00:00 2001 From: Adrian Lopez Date: Fri, 9 Sep 2016 08:53:05 +0200 Subject: [PATCH 3/3] Match also in groups --- src/skype.coffee | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/skype.coffee b/src/skype.coffee index 56e2d09..ddd2789 100644 --- a/src/skype.coffee +++ b/src/skype.coffee @@ -82,7 +82,7 @@ class Skype extends Adapter # The matches function needs a regexp for the first arguments, then an array of anonymous funcs # Those anonymous functions receive session param, which we could use to answer, store values, etc. # https://docs.botframework.com/en-us/node/builder/chat-reference/classes/_botbuilder_d_.session.html - @intents.matches /example$/i, [ + @intents.matches /.*example$/i, [ (session) => session.send "This bot is a mixture between BotFramwork de Microsoft y Hubot. If you write 'example', this message is shown.\n\n @@ -94,7 +94,7 @@ In group chats, bot only will receive messages send to it. Eg.: @botname example # This example intent has two anonymous funcs. First one start a dialog (botframework function) with the user. # Second one is executed after and receive the values written by the user - @intents.matches /chat$/i, [ + @intents.matches /.*chat$/i, [ (session) => session.beginDialog '/chat' return @@ -113,7 +113,7 @@ In group chats, bot only will receive messages send to it. Eg.: @botname example ] # If user wants to exit from any dialog at any moment it can write "goodbye" - @bot.endConversationAction('goodbye', 'Closing dialog', { matches: /^goodbye/i }); + @bot.endConversationAction('goodbye', 'Closing dialog', { matches: /.*goodbye/i }); # This dialog receives as first argument a name (to be called from intents) and an array of anonymous functions (as seen with intents) # Normally you send answers and receive responses in the next func