├── index.js ├── package.json ├── .gitignore ├── README.md └── viber-connector.js /index.js: -------------------------------------------------------------------------------- 1 | var viberConnector = require('./viber-connector.js') 2 | 3 | exports.ViberEnabledConnector = viberConnector.ViberEnabledConnector; 4 | exports.ViberChannelId = viberConnector.ViberChannelId -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "botbuilder-viber", 3 | "version": "0.0.3", 4 | "description": "Viber connector for Microsoft BotBuilder framework", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "botbuilder", 11 | "viber", 12 | "microsoft", 13 | "chatbot" 14 | ], 15 | "author": "DreamTeam Mobile", 16 | "license": "ISC", 17 | "devDependencies": { 18 | "botbuilder": "3.9.0", 19 | "viber-bot": "1.0.12", 20 | "winston": "2.3.0", 21 | "winston-console-formatter": "0.3.1" 22 | }, 23 | "dependencies": { 24 | "async": "2.1.5", 25 | "botbuilder": "^3.9.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | botbuilder-viber 2 | ======= 3 | NPM version 4 | NPM downloads 5 | 6 | A node module for Microsoft [BotBuilder Framework](https://github.com/Microsoft/BotBuilder). 7 | 8 | This module provides plug-in Viber connector for Microsoft BotBuilder framework. 9 | 10 | `botbuilder-viber` currently supports the following tracking features: 11 | 12 | * sending text, image, buttons, stickers messages 13 | * receiving text messages 14 | * receiving images 15 | * receiving other messages as text message `[json]` with `message.attachments[0]` filled with raw Viber message: 16 | ``` 17 | { 18 | payload: rawMessage, /* json from viber directly */ 19 | contentType: 'object', 20 | } 21 | ``` 22 | 23 | ## Getting started 24 | 25 | `botbuilder-viber` is installed just like any other node module: 26 | 27 | ``` 28 | $ npm i botbuilder-viber 29 | ``` 30 | In your bot's app.js: 31 | ``` 32 | var viber = require('botbuilder-viber') 33 | ... 34 | var viberOptions = { 35 | Token: process.env.VIBER_TOKEN, 36 | Name: 'ViberBotName', 37 | AvatarUrl: 'http://url.to/pngfile' 38 | } 39 | var viberChannel = new viber.ViberEnabledConnector(viberOptions) 40 | //after initialising your bot and existing connectors 41 | bot.connector(viber.ViberChannelId, viberChannel) 42 | app.use('/viber/webhook', viberChannel.listen()) 43 | ``` 44 | 45 | When the bot starts, you need to [register your webhook with Viber](https://developers.viber.com/api/rest-bot-api/index.html#webhooks). 46 | 47 | Url of the webhook will be the url of your bot appended `/viber/webhook`. 48 | 49 | Example: `https://botappservice.azurewebsites.net/viber/webhook` 50 | 51 | `botbuilder-viber` requires `express` instead of `restify` like most `BotBuilder` samples do. 52 | 53 | ## Sending stickers 54 | ``` 55 | var viberPayload = {} 56 | viberPayload[viber.ViberChannelId] = { 57 | "type": "sticker", 58 | "sticker_id": 114406 59 | } 60 | var stickerMessage = new builder.Message(session).sourceEvent(viberPayload) 61 | session.send(stickerMessage) 62 | ``` 63 | [How to find out Viber's sticker ids?](https://developers.viber.com/tools/sticker-ids/index.html) 64 | 65 | 66 | ### Viber API documentation 67 | [Viber REST API](https://developers.viber.com/api/rest-bot-api/index.html) 68 | 69 | ### Other information 70 | * This BotBuilder is working on top of `viber-node` module: https://github.com/Viber/viber-bot-node 71 | * It can probably send whole set of different messages Viber supports (Text, Url, Contact, Picture, Video, Location, Sticker, File) 72 | * Avatar is recommended to be 720x720, and no more than 100kb (otherwise will not show on mobile clients). Use tinypng to make it smaller. -------------------------------------------------------------------------------- /viber-connector.js: -------------------------------------------------------------------------------- 1 | const builder = require('botbuilder'); 2 | const ViberBot = require('viber-bot').Bot; 3 | const BotEvents = require('viber-bot').Events; 4 | const UserProfile = require('viber-bot').UserProfile; 5 | const VTextMessage = require('viber-bot').Message.Text 6 | const VPictureMessage = require('viber-bot').Message.Picture 7 | const VLocationMessage = require('viber-bot').Message.Location 8 | const VContactMessage = require('viber-bot').Message.Contact 9 | const VStickerMessage = require('viber-bot').Message.Sticker 10 | const winston = require('winston'); 11 | const toYAML = require('winston-console-formatter'); // makes the output more friendly 12 | const async = require("async"); 13 | /* 14 | Until BotBuilder supports custom channels, 15 | we have to use Kik's channelId to make BotBuilder play nice with user data. 16 | We can use any other channel which supports buttons instead of Kik here. 17 | */ 18 | const ViberChannelId = 'kik' 19 | 20 | const logger = function() { 21 | const logger = new winston.Logger({ level: "debug" }); // We recommend DEBUG for development 22 | logger.add(winston.transports.Console, toYAML.config()); 23 | return logger; 24 | }(); 25 | 26 | var ViberEnabledConnector = (function() { 27 | function ViberEnabledConnector(opts) { 28 | var self = this; 29 | this.options = opts || {}; 30 | this.viberBot = new ViberBot({ 31 | authToken: this.options.Token, 32 | name: this.options.Name, 33 | // It is recommended to be 720x720, and no more than 100kb. 34 | avatar: this.options.AvatarUrl, 35 | logger: logger 36 | }); 37 | 38 | this.viberBot.on(BotEvents.MESSAGE_RECEIVED, (message, response) => { 39 | self.processMessage(message, response); 40 | }); 41 | 42 | this.viberBot.on(BotEvents.CONVERSATION_STARTED, (response, onFinish) => { 43 | // onFinish(new TextMessage(`Hi, ${userProfile.name}! Nice to meet you.`)) 44 | var self = this; 45 | var userProfile = response.userProfile; 46 | var addr = { 47 | channelId: ViberChannelId, 48 | user: { id: encodeURIComponent(userProfile.id), name: userProfile.name }, 49 | bot: { id: 'viberbot', name: self.options.Name }, 50 | conversation: { id: 'ViberConversationId' } 51 | }; 52 | 53 | var msg = new builder.Message() 54 | .address(addr) 55 | .timestamp(convertTimestamp(new Date())) 56 | .entities(); 57 | msg.type = msg.data.type = 'contactRelationUpdate'; 58 | msg.data.action = 'add'; 59 | this.handler([msg.toMessage()]); 60 | }); 61 | } 62 | 63 | function convertTimestamp(ts) { 64 | return ts; 65 | } 66 | 67 | ViberEnabledConnector.prototype.processMessage = function(message, response) { 68 | var self = this; 69 | var userProfile = response.userProfile; 70 | var addr = { 71 | channelId: ViberChannelId, 72 | user: { id: encodeURIComponent(userProfile.id), name: userProfile.name }, 73 | bot: { id: 'viberbot', name: self.options.Name }, 74 | conversation: { id: 'ViberConversationId' } 75 | }; 76 | var msg = new builder.Message() 77 | .address(addr) 78 | .timestamp(convertTimestamp(message.timestamp)) 79 | .entities(); 80 | 81 | var rawMessage = message.toJson(); 82 | if (rawMessage.type === 'text') { 83 | msg = msg.text(message.text); 84 | } else if (rawMessage.type === 'picture'){ 85 | msg.text(message.text || 'picture').addAttachment({ 86 | contentUrl: rawMessage.media, 87 | contentType: 'image/jpeg', 88 | name: 'viberimage.jpeg' 89 | }) 90 | } else { 91 | msg = msg.text(message.text || '[json]').addAttachment({ 92 | payload: rawMessage, 93 | contentType: 'object', 94 | }); 95 | } 96 | this.handler([msg.toMessage()]); 97 | return this; 98 | } 99 | 100 | ViberEnabledConnector.prototype.onEvent = function(handler) { 101 | this.handler = handler; 102 | } 103 | 104 | ViberEnabledConnector.prototype.listen = function() { 105 | return this.viberBot.middleware(); 106 | } 107 | 108 | ViberEnabledConnector.prototype.send = function(messages, done) { 109 | var _this = this; 110 | async.eachSeries(messages, function(msg, cb) { 111 | try { 112 | if (msg.address) { 113 | _this.postMessage(msg, cb); 114 | } else { 115 | logger.error('ViberEnabledConnector: send - message is missing address.'); 116 | cb(new Error('Message missing address.')); 117 | } 118 | } catch (e) { 119 | cb(e); 120 | } 121 | }, done); 122 | } 123 | 124 | ViberEnabledConnector.prototype.convertToViberMessage = function(message) { 125 | var viberKb = null; 126 | var pictureMessage = null; 127 | if (message.sourceEvent && message.sourceEvent.type) { 128 | switch (message.sourceEvent.type) { 129 | case 'sticker': 130 | return new VStickerMessage(message.sourceEvent.sticker_id, null, null, new Date(), '') 131 | break; 132 | } 133 | } 134 | if (message.attachments && message.attachments.length) { 135 | var attachment = message.attachments[0]; 136 | switch (attachment.contentType) { 137 | case 'application/vnd.microsoft.keyboard': 138 | var a = attachment; 139 | if (a.content.buttons && a.content.buttons.length) { 140 | viberKb = { 141 | "Type": "keyboard", 142 | "DefaultHeight": true, 143 | "Buttons": [] 144 | }; 145 | for (var j = 0; j < a.content.buttons.length; j++) { 146 | var sourceB = a.content.buttons[j]; 147 | var b = { 148 | "ActionType": "reply", 149 | "ActionBody": sourceB.value, 150 | "Text": sourceB.title, 151 | "TextSize": "regular" 152 | }; 153 | viberKb.Buttons.push(b); 154 | } 155 | } 156 | break; 157 | case 'image/jpeg': 158 | var p = attachment; 159 | pictureMessage = new VPictureMessage(p.contentUrl, message.text, null, null, null, new Date(), ''); 160 | break; 161 | } 162 | } 163 | if (pictureMessage) { 164 | return pictureMessage; 165 | } else { 166 | return new VTextMessage(message.text, viberKb, null, new Date(), '') 167 | } 168 | } 169 | 170 | ViberEnabledConnector.prototype.postMessage = function(message, cb) { 171 | var self = this, 172 | addr = message.address, 173 | user = addr.user; 174 | var realUserId = decodeURIComponent(addr.user.id) 175 | var profile = new UserProfile(realUserId, addr.user.name, '', '', ''); 176 | if (message.type === 'typing') { 177 | // since Viber doesn't support "typing" notifications via API 178 | // this.viberBot.sendMessage(profile, [new VTextMessage('...', null, null, new Date(), '')]).then(function(x) { cb()}, function(y) {cb()}) 179 | cb() 180 | } else { 181 | var viberMessages = [self.convertToViberMessage(message)]; 182 | this.viberBot.sendMessage(profile, viberMessages).then(function(x) { cb() }, function(y) { cb() }) 183 | } 184 | } 185 | 186 | ViberEnabledConnector.prototype.startConversation = function(address, done) { 187 | var addr = address 188 | address.conversation = { id: 'ViberConversationId' } 189 | done(null, addr); 190 | } 191 | 192 | return ViberEnabledConnector; 193 | })(); 194 | 195 | exports.ViberEnabledConnector = ViberEnabledConnector; 196 | exports.ViberChannelId = ViberChannelId --------------------------------------------------------------------------------