├── .gitignore ├── package.json ├── LICENSE ├── tests └── example.js ├── README.md └── WechatConnector.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-wechat-connector", 3 | "version": "1.0.2", 4 | "description": "Microsoft Bot Framework V3 connector for Wechat office account", 5 | "main": "WechatConnector.js", 6 | "scripts": { 7 | "test": "node tests/example.js" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/jyfcrw/botbuilder-wechat-connector.git" 12 | }, 13 | "keywords": [ 14 | "weixin", 15 | "wechat", 16 | "botbuilder", 17 | "connector" 18 | ], 19 | "author": "Orwme", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/jyfcrw/botbuilder-wechat-connector/issues" 23 | }, 24 | "homepage": "https://github.com/jyfcrw/botbuilder-wechat-connector", 25 | "dependencies": { 26 | "botbuilder": "^3.2.0", 27 | "wechat-api": "^1.28.0", 28 | "wechat": "^2.0.3", 29 | "lodash": "^4.15.0" 30 | }, 31 | "devDependencies": { 32 | "express": "^4.14.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Rong.W 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/example.js: -------------------------------------------------------------------------------- 1 | var express = require('express'), 2 | builder = require('botbuilder'), 3 | connector = require('../WechatConnector'); 4 | 5 | // Create http server 6 | var app = express(); 7 | 8 | // Create wechat connector 9 | var wechatConnector = new connector.WechatConnector({ 10 | appID: "YOUR WECHAT APP ID", 11 | appSecret: "YOUR WECHAT APP SECRET", 12 | appToken: "YOUR WECHAT TOKEN" 13 | }); 14 | 15 | var bot = new builder.UniversalBot(wechatConnector); 16 | 17 | // Bot dialogs 18 | bot.dialog('/', [ 19 | function (session) { 20 | if (session.userData && session.userData.name) { 21 | if (session.message.attachments && 22 | session.message.attachments.length > 0) { 23 | var atm = session.message.attachments[0]; 24 | if (atm.contentType == connector.WechatAttachmentType.Image) { 25 | var msg = new builder.Message(session).attachments([atm]); 26 | session.send(msg); 27 | } 28 | } 29 | session.send("How are you, " + session.userData.name); 30 | } else { 31 | builder.Prompts.text(session, "What's your name?"); 32 | } 33 | }, 34 | function (session, results) { 35 | session.userData.name = results.response; 36 | session.send("OK, " + session.userData.name); 37 | builder.Prompts.text(session, "What's your age?"); 38 | }, 39 | function (session, results) { 40 | session.userData.age = results.response; 41 | session.send("All right, " + results.response); 42 | } 43 | ]); 44 | 45 | app.use('/bot/wechat', wechatConnector.listen()); 46 | 47 | app.get('*', function(req, res) { 48 | res.send(200, 'Hello Wechat Bot'); 49 | }); 50 | 51 | // Start listen on port 52 | app.listen(process.env.port || 9090, function() { 53 | console.log('server is running.'); 54 | }); 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # botbuilder-wechat-connector 2 | Microsoft Bot Framework V3 connector for Wechat office account 3 | 4 | [![npm version](https://badge.fury.io/js/botbuilder-wechat-connector.svg)](https://badge.fury.io/js/botbuilder-wechat-connector) 5 | [![dependencies Status](https://david-dm.org/jyfcrw/botbuilder-wechat-connector/status.svg)](https://david-dm.org/jyfcrw/botbuilder-wechat-connector) 6 | 7 | ## Features 8 | 9 | * ready for Microsoft Bot Framework V3 10 | * **no need a registered bot** on [dev.botframework.com](https://dev.botframework.com/), but require a certified wechat office account, go to apply [trial account](http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login) 11 | * depend on [wechat](https://github.com/node-webot/wechat) and [wechat-api](https://github.com/node-webot/wechat-api) packages 12 | * support receiving and sending almost any wechat message types 13 | * for [express](http://expressjs.com/) framework 14 | 15 | ## Installation 16 | 17 | ``` 18 | npm install botbuilder-wechat-connector 19 | ``` 20 | 21 | ## Preparation 22 | 23 | We assume that, you don't have a certified wechat office account yet, but want to use a trial account for API testing, go to [this place](http://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=sandbox/login) to apply a trial wechat office account. 24 | 25 | If you do have a certified wechat office account, you must know how to configure wechat message service certainly. 26 | 27 | ## Usage 28 | 29 | **Step 1**, create your bot with wechat connector 30 | ``` 31 | var builder = require('botbuilder'), 32 | connector = require('botbuilder-wechat-connector'); 33 | 34 | var wechatConnector = new connector.WechatConnector({ 35 | appID: "YOUR WECHAT APP ID", 36 | appSecret: "YOUR WECHAT APP SECRET", 37 | appToken: "YOUR WECHAT TOKEN" 38 | }); 39 | 40 | var bot = new builder.UniversalBot(wechatConnector); 41 | ``` 42 | 43 | **Step 2**, create express app as usual and use wechat connector as middleware 44 | ``` 45 | var app = express(); 46 | app.use('/bot/wechat', wechatConnector.listen()); 47 | app.listen(9090); 48 | ``` 49 | 50 | when you configure your wechat message service, you have to offer an available public url, if can not, try [ngrok](https://ngrok.com/). When submit this url in wechat backend, wechat server will send request to this url, so, ensure you server running good before submiting. 51 | 52 | **Step 3**, add dialogs and you can see `message` in session object include wechat message content you sent. 53 | ``` 54 | bot.dialog('/', function (session) { 55 | console.log('Wechat message: ', session.message); 56 | }); 57 | ``` 58 | And, you can find media content like image, voice, video, etc in `message.attachments` of session object. 59 | ``` 60 | bot.dialog('/', function (session) { 61 | console.log('Wechat media: ', session.message.attachments); 62 | }); 63 | ``` 64 | 65 | for now, we include all wechat message type, as follow. 66 | ``` 67 | console.log(connector.WechatAttachmentType) 68 | 69 | { 70 | Image: 'wechat/image', 71 | Voice: 'wechat/voice', 72 | Video: 'wechat/video', 73 | ShortVideo: 'wechat/shortvideo', 74 | Link: 'wechat/link', 75 | Location: 'wechat/location', 76 | Music: 'wechat/music', 77 | News: 'wechat/news', 78 | MpNews: 'wechat/mpnews', 79 | Card: 'wechat/card' 80 | } 81 | ``` 82 | 83 | **Step 4**, sending message out is the same. 84 | 85 | Send text message 86 | ``` 87 | bot.dialog('/', function (session) { 88 | session.send("Im a wechat bot!"); 89 | }); 90 | ``` 91 | 92 | Send media, like image, video, etc. for example, send an image out. 93 | 94 | ``` 95 | bot.dialog('/', function (session) { 96 | var msg = new builder.Message(session).attachments([ 97 | { 98 | contentType: 'wechat/image', 99 | content: { 100 | mediaId: 'MEDIA ID OF AN IMAGE' 101 | } 102 | } 103 | ]); 104 | session.send(msg); 105 | }); 106 | ``` 107 | 108 | Want know more about `content` object, please reference this [offical document](http://mp.weixin.qq.com/wiki/11/c88c270ae8935291626538f9c64bd123.html). 109 | 110 | ### Attachment 111 | 112 | Here is detail of all attachment object scheme. 113 | 114 | #### Send attachment 115 | 116 | **Image** 117 | ``` 118 | { 119 | contentType: 'wechat/image', 120 | content: { 121 | mediaId: 'MEDIA ID OF AN IMAGE' 122 | } 123 | } 124 | ``` 125 | 126 | **Voice** 127 | ``` 128 | { 129 | contentType: 'wechat/voice', 130 | content: { 131 | mediaId: 'MEDIA ID OF VOICE' 132 | } 133 | } 134 | ``` 135 | 136 | **Video** 137 | ``` 138 | { 139 | contentType: 'wechat/video', 140 | content: { 141 | mediaId: 'MEDIA ID OF VIDEO', 142 | thumbMediaId: 'MEDIA ID OF THUMB' 143 | } 144 | } 145 | ``` 146 | 147 | **Music** 148 | ``` 149 | { 150 | contentType: 'wechat/music', 151 | content: { 152 | title: 'TITLE', 153 | description: 'DESC', 154 | musicurl: 'MUSIC URL', 155 | hqmusicurl: "HQ MUSIC URL", 156 | thumb_media_id: "THUMB MEDIA ID" 157 | } 158 | } 159 | ``` 160 | 161 | **News** 162 | ``` 163 | { 164 | contentType: 'wechat/news', 165 | content: [ 166 | { 167 | "title": "TITLE", 168 | "description": "DESC", 169 | "url": "NEWS URL", 170 | "picurl": "PIC URL" 171 | }, 172 | ... 173 | ] 174 | } 175 | ``` 176 | 177 | **MpNews** 178 | ``` 179 | { 180 | contentType: 'wechat/mpnews', 181 | content: { 182 | mediaId: 'MEDIA ID OF MPNEWS' 183 | } 184 | } 185 | ``` 186 | 187 | **Card** 188 | ``` 189 | { 190 | contentType: 'wechat/card', 191 | content: { 192 | card_id: 'CARD ID', 193 | card_ext: 'CARD EXT' 194 | } 195 | } 196 | ``` 197 | 198 | #### Receive attachment 199 | 200 | **Image** 201 | ``` 202 | { 203 | contentType: 'wechat/image', 204 | content: { 205 | url: 'IMAGE URL', 206 | mediaId: 'MEDIA ID OF AN IMAGE' 207 | } 208 | } 209 | ``` 210 | 211 | **Voice** 212 | ``` 213 | { 214 | contentType: 'wechat/voice', 215 | content: { 216 | format: 'FORMAT', 217 | recognition: 'RECOGNITION', 218 | mediaId: 'MEDIA ID OF VOICE' 219 | } 220 | } 221 | ``` 222 | 223 | **Video** 224 | ``` 225 | { 226 | contentType: 'wechat/video', 227 | content: { 228 | mediaId: 'MEDIA ID OF VIDEO', 229 | thumbMediaId: 'MEDIA ID OF THUMB' 230 | } 231 | } 232 | ``` 233 | 234 | **Short Video** 235 | Only receiving messages can have this message type. 236 | ``` 237 | { 238 | contentType: 'wechat/shortvideo', 239 | content: { 240 | mediaId: 'MEDIA ID OF SHORT VIDEO', 241 | thumbMediaId: 'MEDIA ID OF THUMB' 242 | } 243 | } 244 | ``` 245 | 246 | **Link** 247 | ``` 248 | { 249 | contentType: 'wechat/link', 250 | content: { 251 | title: 'TITLE', 252 | description: 'DESC', 253 | url: 'LINK URL' 254 | } 255 | } 256 | ``` 257 | 258 | **Location** 259 | ``` 260 | { 261 | contentType: 'wechat/location', 262 | content: { 263 | locationX: 'LOCATIONX', 264 | locationY: 'LOCATIONY', 265 | scale: 'SCALE', 266 | label: 'LABEL' 267 | } 268 | } 269 | ``` 270 | 271 | ## Example 272 | An example is located at tests directory. Using following command to run it. 273 | 274 | ``` 275 | npm test 276 | ``` 277 | 278 | ## Thanks 279 | 280 | This package is greatly inspired by [botbuilder-wechat](https://github.com/markusf/botbuilder-wechat), so thanks @markusf. 281 | 282 | ## Issues 283 | 284 | Please feel free to open issues, if you have any suggestion. 285 | 286 | ## License 287 | 288 | The MIT license 289 | -------------------------------------------------------------------------------- /WechatConnector.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require('lodash'), 4 | builder = require('botbuilder'), 5 | wechat = require('wechat'), 6 | WechatAPI = require('wechat-api'); 7 | 8 | const AttachmentType = { 9 | Image: 'wechat/image', 10 | Voice: 'wechat/voice', 11 | Video: 'wechat/video', 12 | ShortVideo: 'wechat/shortvideo', 13 | Link: 'wechat/link', 14 | Location: 'wechat/location', 15 | Music: 'wechat/music', 16 | News: 'wechat/news', 17 | MpNews: 'wechat/mpnews', 18 | Card: 'wechat/card' 19 | }; 20 | 21 | var WechatConnector = (function() { 22 | function WechatConnector(opts) { 23 | this.options = _.assign({ 24 | enableReply: false 25 | }, opts); 26 | 27 | this.wechatAPI = new WechatAPI(this.options.appID, this.options.appSecret); 28 | } 29 | 30 | WechatConnector.prototype.listen = function () { 31 | var self = this; 32 | var config = this.options.appToken; 33 | 34 | if (!!this.options.encodingAESKey) { 35 | config = { 36 | token: this.options.appToken, 37 | appid: this.options.appID, 38 | encodingAESKey: this.options.encodingAESKey 39 | }; 40 | } 41 | 42 | return wechat(config, function(req, res, next) { 43 | var wechatMessage = req.weixin; 44 | 45 | if (!self.options.enableReply) { 46 | self.processMessage(wechatMessage); 47 | res.status(200).end(); 48 | } else { 49 | next(); 50 | } 51 | }); 52 | }; 53 | 54 | WechatConnector.prototype.processMessage = function (wechatMessage) { 55 | var msg, 56 | addr, 57 | atts = [], 58 | msgType = wechatMessage.MsgType; 59 | 60 | if (!this.handler) { 61 | throw new Error('Error no handler'); 62 | } 63 | 64 | addr = { 65 | channelId: 'wechat', 66 | user: { id: wechatMessage.FromUserName, name: 'Unknown' }, 67 | bot: { id: wechatMessage.ToUserName, name: 'Bot' }, 68 | conversation: { id: 'Convo1' } 69 | }; 70 | 71 | msg = new builder.Message() 72 | .address(addr) 73 | .timestamp(convertTimestamp(wechatMessage.CreateTime)) 74 | .entities(); 75 | 76 | if (msgType === 'text') { 77 | msg = msg.text(wechatMessage.Content); 78 | } else if (msgType == 'voice') { 79 | if (wechatMessage.Recognition) { 80 | msg = msg.text(wechatMessage.Recognition) 81 | } else { 82 | msg = msg.text(''); 83 | } 84 | } else{ 85 | msg = msg.text(''); 86 | } 87 | 88 | if (msgType === 'image') { 89 | atts.push({ 90 | contentType: AttachmentType.Image, 91 | content: { 92 | url: wechatMessage.PicUrl, 93 | mediaId: wechatMessage.MediaId 94 | } 95 | }); 96 | } 97 | 98 | if (msgType === 'voice') { 99 | atts.push({ 100 | contentType: AttachmentType.Voice, 101 | content: { 102 | format: wechatMessage.Format, 103 | mediaId: wechatMessage.MediaId, 104 | recognition: wechatMessage.Recognition 105 | } 106 | }); 107 | } 108 | 109 | if (msgType === 'video') { 110 | atts.push({ 111 | contentType: AttachmentType.Video, 112 | content: { 113 | mediaId: wechatMessage.MediaId, 114 | thumbMediaId: wechatMessage.ThumbMediaId 115 | } 116 | }); 117 | } 118 | 119 | if (msgType === 'shortvideo') { 120 | atts.push({ 121 | contentType: AttachmentType.ShortVideo, 122 | content: { 123 | mediaId: wechatMessage.MediaId, 124 | thumbMediaId: wechatMessage.ThumbMediaId 125 | } 126 | }); 127 | } 128 | 129 | if (msgType === 'link') { 130 | atts.push({ 131 | contentType: AttachmentType.Link, 132 | content: { 133 | title: wechatMessage.Title, 134 | description: wechatMessage.Description, 135 | url: wechatMessage.Url 136 | } 137 | }); 138 | } 139 | 140 | if (msgType === 'location') { 141 | atts.push({ 142 | contentType: AttachmentType.Location, 143 | content: { 144 | locationX: wechatMessage.Location_X, 145 | locationY: wechatMessage.Location_Y, 146 | scale: wechatMessage.Scale, 147 | label: wechatMessage.Label 148 | } 149 | }); 150 | } 151 | 152 | msg = msg.attachments(atts); 153 | this.handler([msg.toMessage()]); 154 | return this; 155 | }; 156 | 157 | WechatConnector.prototype.onEvent = function (handler) { 158 | this.handler = handler; 159 | }; 160 | 161 | WechatConnector.prototype.send = function (messages, cb) { 162 | for (var i = 0; i < messages.length; i++) { 163 | this.postMessage(messages[i]); 164 | } 165 | }; 166 | 167 | WechatConnector.prototype.startConversation = function (address, cb) { 168 | var addr = _.assign(address, { 169 | conversation: { id: 'Convo1' } 170 | }); 171 | 172 | cb(null, addr); 173 | }; 174 | 175 | WechatConnector.prototype.postMessage = function (message, cb) { 176 | var self = this, 177 | addr = message.address, 178 | user = addr.user; 179 | 180 | if (message.text && message.text.length > 0) { 181 | this.wechatAPI.sendText(user.id, message.text, errorHandle); 182 | } 183 | 184 | if (message.attachments && message.attachments.length > 0) { 185 | for (var i = 0; i < message.attachments.length; i++) { 186 | var atm = message.attachments[i], 187 | atmType = atm.contentType, 188 | atmCont = atm.content; 189 | 190 | if (!atmCont) continue; 191 | 192 | switch(atmType) { 193 | case AttachmentType.Image: 194 | this.wechatAPI.sendImage(user.id, atmCont.mediaId, errorHandle); 195 | break; 196 | case AttachmentType.Voice: 197 | this.wechatAPI.sendVoice(user.id, atmCont.mediaId, errorHandle); 198 | break; 199 | case AttachmentType.Video: 200 | this.wechatAPI.sendVideo(user.id, atmCont.mediaId, atmCont.thumbMediaId, errorHandle); 201 | break; 202 | case AttachmentType.Music: 203 | this.wechatAPI.sendMusic(user.id, atmCont, errorHandle); 204 | break; 205 | case AttachmentType.News: 206 | this.wechatAPI.sendNews(user.id, atmCont, errorHandle); 207 | break; 208 | case AttachmentType.MpNews: 209 | this.wechatAPI.sendMpNews(user.id, atmCont.mediaId, errorHandle); 210 | break; 211 | case AttachmentType.Card: 212 | this.wechatAPI.sendCard(user.id, atmCont, errorHandle); 213 | break; 214 | default: 215 | // Unknown attachment 216 | break; 217 | } 218 | } 219 | } 220 | }; 221 | 222 | function errorHandle(err) { 223 | if (err) { 224 | console.log('Error', err); 225 | } 226 | } 227 | 228 | function convertTimestamp(ts) { 229 | return new Date(parseInt(ts) * 1000).toISOString(); 230 | } 231 | 232 | return WechatConnector; 233 | })(); 234 | 235 | exports.WechatConnector = WechatConnector; 236 | exports.WechatAttachmentType = AttachmentType; --------------------------------------------------------------------------------