diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..5bc9e42 --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,40 @@ +module.exports = function(grunt) { + grunt.initConfig({ + babel: { + scripts: { + files: [{ + expand: true, + cwd: 'lib', + src: '**/*.js', + dest: 'build/' + }] + } + }, + eslint: { + scripts: ['lib/**/*.js'] + }, + symlink: { + classes: { + files: [{ + expand: true, + cwd: 'build/types', + src: '*', + dest: 'types' + }] + } + }, + watch: { + scripts: { + files: ['lib/**/*.js'], + tasks: ['babel'] + } + } + }); + + grunt.loadNpmTasks('grunt-babel'); + grunt.loadNpmTasks('grunt-contrib-watch'); + grunt.loadNpmTasks('grunt-contrib-symlink'); + grunt.loadNpmTasks('grunt-eslint'); + + grunt.registerTask('default', ['babel', 'symlink', 'eslint']); +}; diff --git a/README.md b/README.md index 92e0619..2f2be85 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Create and control [Telegram bots](https://core.telegram.org/bots) easily using the new [Telegram API](https://core.telegram.org/bots/api). -telegram-api is in beta, your feedback is highly appreciated, please fill an issue +telegram-api is in beta, your feedback is appreciated, please [fill an issue](https://github.com/mdibaiee/node-telegram-api/issues) for any bugs you find or any suggestions you have. ``` npm install telegram-api @@ -10,11 +10,16 @@ npm install telegram-api # Example Take a look at [demo.js](https://github.com/mdibaiee/node-telegram-api/blob/master/demo.js). -Also [@JavaScriptBot](https://telegram.me/JavaScriptBot), still work in progress. + +[@JavaScriptBot](https://telegram.me/JavaScriptBot) runs on `demo.js`, you can test it. ```javascript var Bot = require('telegram-api'); +// only require the message types you need, more coming soon! +var Message = require('telegram-api/types/Message'); +var Question = require('telegram-api/types/Question'); + var smartBot = new Bot({ token: 'YOUR_KEY' }); @@ -24,40 +29,62 @@ smartBot.start().then(() => { console.log(smartBot.info); }); -// You can use regular expressions, too -smartBot.get('Hi', function(update) { +// Create a new question +// answers is a keyboard layout as defined in Telegram API +// we're going to reuse this by modifying it's target +const question = new Question() + .text('How should I greet you?') + .answers([['Hey'], ['Hello, Sir'], ['Yo bro']]); + +// Called when a message starting with Hi is received +// You can use Regular Expressions, too +// update is an Update object as defined in Telegram API +smartBot.get('Hi', update => { const message = update.message; - const id = message.chat.id; - // answers is in format of keyboard rows - const question = 'How should I greet you?', - answers = [['Hi'], ['Hello, Sir'], ['Yo bro']]; + question.to(message.chat.id).reply(message.message_id); - smartBot.replyTo(message.message_id) - .askQuestion(id, question, answers) - .then(answer => { - smartBot.message(id, 'Your answer: ' + answer); + // Send the question, returns a promise, resolves on valid answer, + // rejects in case of an invalid answer + smartBot.send(question).then(answer => { + const msg = new Message().to(id).text('Your answer: ' + answer); + smartBot.send(msg); }, () => { - smartBot.message(id, 'Invalid answer'); + const msg = new Message().to(id).text('Invalid answer'); + smartBot.send(msg); }); }); -// Commands are in format `/command` or `/command@botusername` in groups +// Commands are in the format `/command` or `/command@botusername` in groups +const test = new Message().text('Test Command'); smartBot.command('test', update => { const message = update.message; const id = message.chat.id; - smartBot.message(id, 'Test command'); + smartBot.send(test.to(id)); }); +const hello = new Message().text('Hello'); smartBot.command('start', update => { - smartBot.message(update.message.chat.id, 'Hello!'); + smartBot.send(hello.to(update.message.chat.id)); }); -// You can access all API methods through the api property until we implement -// easier methods -smartBot.api.getUserProfilePhotos ``` This will result in: ![@JavaScriptBot](https://github.com/mdibaiee/node-telegram-api/raw/master/demo.gif) + + +# Bots using this module + +[@JavaScriptBot](https://telegram.me/JavaScriptBot) + +# Todo + +- [] BulkMessage Type +- [] File Type +- [] Sticker Type +- [] Location Type +- [] Contact Type +- [] Allow remote control of bots (TCP maybe) +- YOUR IDEAS! [Fill an issue](https://github.com/mdibaiee/node-telegram-api/issues) diff --git a/build/api.js b/build/api.js new file mode 100644 index 0000000..f232f2e --- /dev/null +++ b/build/api.js @@ -0,0 +1,49 @@ +// API methods +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +var _fetch = require('./fetch'); + +var _fetch2 = _interopRequireDefault(_fetch); + +/** + * API class, has a function for each method of the Telegram API which take + * an object argument, and send request to the API server + * + * Methods: getMe, sendMessage, forwardMessage, sendPhoto, sendAudio, + * sendDocument, sendSticker, sendVideo, sendLocation, sendChatAction, + * getUserProfilePhotos, getUpdates + */ + +var API = +/** + * Create a new api object with the given token + * @param {string} token + */ +function API(token) { + _classCallCheck(this, API); + + this.token = token; +}; + +exports['default'] = API; + +API.prototype.request = function request(method, data) { + return (0, _fetch2['default'])(this.token + '/' + method, data); +}; + +var methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto', 'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo', 'sendLocation', 'sendChatAction', 'getUserProfilePhotos', 'getUpdates']; + +methods.forEach(function (method) { + API.prototype[method] = function (data) { + return this.request(method, data); + }; +}); +module.exports = exports['default']; diff --git a/build/fetch.js b/build/fetch.js new file mode 100644 index 0000000..134a2b5 --- /dev/null +++ b/build/fetch.js @@ -0,0 +1,53 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports['default'] = fetch; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _https = require('https'); + +var _https2 = _interopRequireDefault(_https); + +var _qs = require('qs'); + +var _qs2 = _interopRequireDefault(_qs); + +function fetch(path, data) { + var post = _qs2['default'].stringify(data); + + return new Promise(function (resolve, reject) { + var res = ''; + + var req = _https2['default'].request({ + hostname: 'api.telegram.org', + method: data ? 'POST' : 'GET', + path: '/bot' + path, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, function (response) { + response.on('data', function (chunk) { + res += chunk; + }); + + response.on('end', function () { + try { + var json = JSON.parse(res); + resolve(json); + } catch (e) { + reject(e); + } + }); + }).on('error', reject); + + if (post) { + req.write(post); + } + req.end(); + }); +} + +module.exports = exports['default']; diff --git a/build/index.js b/build/index.js new file mode 100644 index 0000000..1676429 --- /dev/null +++ b/build/index.js @@ -0,0 +1,184 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + +require('babel/polyfill'); + +var _api = require('./api'); + +var _api2 = _interopRequireDefault(_api); + +var _events = require('events'); + +var DEFAULTS = { + update: { + offset: 0, + timeout: 0.5, + limit: 100 + } +}; + +/** + * Bot class used to connect to a new bot + * Bots have an api property which gives access to all Telegram API methods, + * see API class + */ + +var Bot = (function (_EventEmitter) { + /** + * Create and connect to a new bot + * @param {object} options Bot properties. + */ + + function Bot() { + var options = arguments[0] === undefined ? { update: {} } : arguments[0]; + + _classCallCheck(this, Bot); + + _get(Object.getPrototypeOf(Bot.prototype), 'constructor', this).call(this); + + if (!options.token) { + throw new Error('Token cannot be empty'); + } + + this.token = options.token; + this.update = Object.assign(options.update || {}, DEFAULTS.update); + + this.api = new _api2['default'](this.token); + + this.msg = {}; + + // EventEmitter + this._events = {}; + this._userEvents = []; + } + + _inherits(Bot, _EventEmitter); + + _createClass(Bot, [{ + key: 'start', + + /** + * Gets information about the bot and then starts polling updates from API + * Emits an `update` event after polling with the response from server + * Returns a promise which is resolved after the bot information is received + * and set to it's `info` property i.e. bot.info + * @return {promise} A promise which is resolved with the response of getMe + */ + value: function start() { + var _this2 = this; + + var poll = (function () { + var _this = this; + + this.api.getUpdates(this.update).then(function (response) { + setTimeout(poll, _this.update.timeout * 1000); + + var result = response.result; + if (!result.length) { + return; + } + + if (!_this.update.offset) { + var updateId = result[result.length - 1].update_id; + _this.update.offset = updateId; + } + if (_this.update) { + _this.update.offset += 1; + } + + _this.emit('update', response.result); + result.forEach(function (res) { + var text = res.message.text; + if (text.startsWith('/')) { + // Commands are sent in /command@botusername format in groups + var regex = new RegExp('@' + _this.info.username + '$'); + text = text.replace(regex, ''); + } + + var ev = _this._userEvents.find(function (_ref) { + var pattern = _ref.pattern; + return pattern.test(text); + }); + ev.listener(res); + }); + }); + }).bind(this); + + return this.api.getMe().then(function (response) { + _this2.info = response.result; + poll(); + }); + } + }, { + key: 'get', + + /** + * Listens on specific message matching the pattern which can be an string + * or a regexp. + * @param {string/regex} pattern + * @param {function} listener function to call when a message matching the + * pattern is found, gets the Update + * In case of string, the message should start + * with the string i.e. /^yourString/ + * @return {object} returns the bot object + */ + value: function get(pattern, listener) { + if (typeof pattern === 'string') { + pattern = new RegExp('^' + pattern); + } + + this._userEvents.push({ + pattern: pattern, listener: listener + }); + + return this; + } + }, { + key: 'command', + + /** + * Listens on a command + * @param {string} cmd the command string, should not include slash (/) + * @param {function} listener function to call when the command is received, + * gets the update + * @return {object} returns the bot object + */ + value: function command(cmd, listener) { + this._userEvents.push({ + pattern: new RegExp('/' + cmd), + listener: listener + }); + + return this; + } + }, { + key: 'send', + + /** + * Sends the message provided + * @param {object} message The message to send. Gets it's send method called + * @return {unknown} returns the result of calling message's send method + */ + value: function send(message) { + return message.send(this); + } + }]); + + return Bot; +})(_events.EventEmitter); + +exports['default'] = Bot; +module.exports = exports['default']; diff --git a/build/types/Base.js b/build/types/Base.js new file mode 100644 index 0000000..fd1a0e1 --- /dev/null +++ b/build/types/Base.js @@ -0,0 +1,66 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + +var _events = require('events'); + +/** + * Base class of all classes + */ + +var Base = (function (_EventEmitter) { + function Base() { + _classCallCheck(this, Base); + + _get(Object.getPrototypeOf(Base.prototype), 'constructor', this).call(this); + this.properties = {}; + } + + _inherits(Base, _EventEmitter); + + _createClass(Base, [{ + key: 'getProperties', + + /** + * Returns properties of the object + * @return {object} properties of object + */ + value: function getProperties() { + return this.properties; + } + }, { + key: 'setProperties', + + /** + * Set properties of the object + * @param {object} object properties to set + * @param {boolean} extend A boolean indicating if the properties should be + * extended by the object provided (Object.assign) + * or properties should be replaced by the object + * defaults to true + * @return {object} returns the properties (same as getProperties) + */ + value: function setProperties(object) { + var extend = arguments[1] === undefined ? true : arguments[1]; + + this.properties = extend ? Object.assign(this.properties, object) : object; + + return this.getProperties(); + } + }]); + + return Base; +})(_events.EventEmitter); + +exports['default'] = Base; +module.exports = exports['default']; diff --git a/build/types/Keyboard.js b/build/types/Keyboard.js new file mode 100644 index 0000000..c1fc8d0 --- /dev/null +++ b/build/types/Keyboard.js @@ -0,0 +1,138 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x6, _x7, _x8) { var _again = true; _function: while (_again) { var object = _x6, property = _x7, receiver = _x8; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x6 = parent; _x7 = property; _x8 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + +var _Base2 = require('./Base'); + +var _Base3 = _interopRequireDefault(_Base2); + +/** + * Keyboard class, used to configure keyboards for messages. + * You should pass your instance of this class to message.keyboard() method + */ + +var Keyboard = (function (_Base) { + /** + * Create a new keyboard + * @param {object} properties Keyboard properties, as defined by Telegram API + * See ReplyKeyboardMarkup, ReplyKeyboardHide, + * ForceReply + */ + + function Keyboard() { + var properties = arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, Keyboard); + + _get(Object.getPrototypeOf(Keyboard.prototype), 'constructor', this).call(this); + + this.properties = properties; + } + + _inherits(Keyboard, _Base); + + _createClass(Keyboard, [{ + key: 'keys', + + /** + * Set the keyboard property of reply_markup + * @param {array} keys An array of arrays, with the format of + * Column Column + * Row [['TopLeft', 'TopRight'], + * Row ['BottomLeft', 'BottomRight']] + * @return {object} returns the keyboard object + */ + value: function keys(_keys) { + this.properties.keyboard = _keys; + this.properties.hide_keyboard = false; + return this; + } + }, { + key: 'force', + + /** + * Set force_keyboard property of reply_markup + * @param {boolean} enable value of force_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + value: function force() { + var enable = arguments[0] === undefined ? true : arguments[0]; + + this.properties.force_keyboard = enable; + return this; + } + }, { + key: 'resize', + + /** + * Set resize_keyboard property of reply_markup + * @param {boolean} enable value of resize_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + value: function resize() { + var enable = arguments[0] === undefined ? true : arguments[0]; + + this.properties.resize_keyboard = enable; + return this; + } + }, { + key: 'oneTime', + + /** + * Set force_keyboard property of reply_markup + * @param {boolean} enable value of force_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + value: function oneTime() { + var enable = arguments[0] === undefined ? true : arguments[0]; + + this.properties.one_time_keyboard = enable; + return this; + } + }, { + key: 'selective', + + /** + * Set selective property of reply_markup + * @param {boolean} enable value of force_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + value: function selective() { + var enable = arguments[0] === undefined ? true : arguments[0]; + + this.properties.selective = enable; + return this; + } + }, { + key: 'hide', + + /** + * Set hide_keyboard property of reply_markup to true + * @return {object} returns the keyboard object + */ + value: function hide() { + this.properties = { + hide_keyboard: true + }; + + return this; + } + }]); + + return Keyboard; +})(_Base3['default']); + +exports['default'] = Keyboard; +module.exports = exports['default']; diff --git a/build/types/Message.js b/build/types/Message.js new file mode 100644 index 0000000..8979a3e --- /dev/null +++ b/build/types/Message.js @@ -0,0 +1,156 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + +var _Base2 = require('./Base'); + +var _Base3 = _interopRequireDefault(_Base2); + +/** + * Message class, used to send message to a chat + */ + +var Message = (function (_Base) { + /** + * Create a new message + * @param {object} properties Message properties, as defined by Telegram API + */ + + function Message() { + var properties = arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, Message); + + _get(Object.getPrototypeOf(Message.prototype), 'constructor', this).call(this); + + this.properties = properties; + this._keyboard = new _Base3['default'](); + } + + _inherits(Message, _Base); + + _createClass(Message, [{ + key: 'to', + + /** + * Set chat_id of the message + * @param {number} chat + * @return {object} returns the message object + */ + value: function to(chat) { + this.properties.chat_id = chat; + return this; + } + }, { + key: 'text', + + /** + * Set text of the message + * @param {string} text Message's content + * @return {object} returns the message object + */ + value: function text(_text) { + this.properties.text = _text; + return this; + } + }, { + key: 'reply', + + /** + * Set reply_to_message_id of the message + * @param {number} id message_id of the message to reply to + * @return {object} returns the message object + */ + value: function reply(id) { + this.properties.reply_to_message_id = id; + return this; + } + }, { + key: 'keyboard', + + /** + * Sets keyboard of the message + * The value of reply_markup is set to the sanitized keyboard properties + * i.e. reply_markup = JSON.stringify(kb.getProperties()) + * @param {object} kb A Keyboard instance + * @return {object} returns the message object + */ + value: function keyboard(kb) { + this._keyboard = kb; + return this; + } + }, { + key: 'send', + + /** + * Sends the message, you should only use this method yourself if + * you are extending this class. Normally you should call bot.send(message) + * + * Events: message:sent => Emitted after sending the message to API, gets the + * API's response + * + * message:answer => Emitted when your message gets an answer from + * the contact (reply in case of groups) + * gets the Update object containing message + * + * @param {object} bot + * @return {promise} returns a promise, resolved with message:answer + */ + value: function send(bot) { + var _this = this; + + var messageId = undefined; + var reply_markup = JSON.stringify(this._keyboard.getProperties()); + this.properties.reply_markup = reply_markup; + + return new Promise(function (resolve) { + bot.api.sendMessage(_this.properties).then(function (response) { + messageId = response.result.message_id; + _this.emit('message:sent', response); + }); + + if (_this._keyboard.one_time_keyboard) { + _this._keyboard.replyMarkup = ''; + } + + var chat = _this.properties.chat_id; + bot.on('update', function listener(result) { + var update = result.find(function (_ref) { + var message = _ref.message; + + // if in a group, there will be a reply to this message + if (chat < 0) { + return message.chat.id === chat && message.reply_to_message.message_id === messageId; + } else { + return message.chat.id === chat; + } + }); + + if (update) { + resolve(update); + this.emit('message:answer', update); + + bot.removeListener('update', listener); + } + }); + }); + } + }]); + + return Message; +})(_Base3['default']); + +exports['default'] = Message; +module.exports = exports['default']; diff --git a/build/types/Question.js b/build/types/Question.js new file mode 100644 index 0000000..e02d762 --- /dev/null +++ b/build/types/Question.js @@ -0,0 +1,112 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); + +var _get = function get(_x2, _x3, _x4) { var _again = true; _function: while (_again) { var object = _x2, property = _x3, receiver = _x4; desc = parent = getter = undefined; _again = false; if (object === null) object = Function.prototype; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x2 = parent; _x3 = property; _x4 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } + +function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } + +var _Message2 = require('./Message'); + +var _Message3 = _interopRequireDefault(_Message2); + +var _Keyboard = require('./Keyboard'); + +var _Keyboard2 = _interopRequireDefault(_Keyboard); + +/** + * Question class, extends Message + * Sends a message, shows a keyboard with the answers provided, and validates + * the answer + */ + +var Question = (function (_Message) { + /** + * Create a new question + * @param {object} options Options, same as Message, plus `answers` which + * is a keyboard layout, see Keyboard#keys + */ + + function Question() { + var options = arguments[0] === undefined ? {} : arguments[0]; + + _classCallCheck(this, Question); + + _get(Object.getPrototypeOf(Question.prototype), 'constructor', this).call(this, options); + + var kb = new _Keyboard2['default']().force().oneTime().selective().keys(this.properties.answers); + this.keyboard(kb); + } + + _inherits(Question, _Message); + + _createClass(Question, [{ + key: 'answers', + + /** + * Sets answers of the question. This is passed to Keyboard#keys, and then + * used to validate the answer given + * @param {array} answers Array of arrays of strings, same as Keyboard#keys + * @return {object} returns the question object + */ + value: function answers(_answers) { + this.answers = _answers; + this._keyboard.keys(_answers); + return this; + } + }, { + key: 'send', + + /** + * Sends the question (same as Message#send), and validates the answer given + * if the answer is one of the defined answers, resolves, else rejects + * You should not manually use this method unless you're extending this class + * You should instead use bot.send(question); + * @param {object} bot + * @return {promise} A promise which is resolved in case of valid answer, and + * rejected in case of invalid answer + */ + value: function send(bot) { + var _this = this; + + var answers = this.answers; + + return new Promise(function (resolve, reject) { + _get(Object.getPrototypeOf(Question.prototype), 'send', _this).call(_this, bot).then(function (update) { + var message = update.message; + var answer = undefined; + + answers.forEach(function find(a) { + if (Array.isArray(a)) { + a.forEach(find); + } + if (a === message.text) { + answer = a; + } + }); + + if (answer) { + resolve(answer, update); + _this.emit('question:answer', answer, update); + } else { + reject(update); + _this.emit('question:invalid', update); + } + }); + }); + } + }]); + + return Question; +})(_Message3['default']); + +exports['default'] = Question; +module.exports = exports['default']; diff --git a/build/utils.js b/build/utils.js new file mode 100644 index 0000000..3918c74 --- /dev/null +++ b/build/utils.js @@ -0,0 +1 @@ +"use strict"; diff --git a/classes/Base.js b/classes/Base.js new file mode 120000 index 0000000..0331749 --- /dev/null +++ b/classes/Base.js @@ -0,0 +1 @@ +../build/classes/Base.js \ No newline at end of file diff --git a/classes/Keyboard.js b/classes/Keyboard.js new file mode 120000 index 0000000..33e9abe --- /dev/null +++ b/classes/Keyboard.js @@ -0,0 +1 @@ +../build/classes/Keyboard.js \ No newline at end of file diff --git a/classes/Message.js b/classes/Message.js new file mode 120000 index 0000000..dd58465 --- /dev/null +++ b/classes/Message.js @@ -0,0 +1 @@ +../build/classes/Message.js \ No newline at end of file diff --git a/classes/Question.js b/classes/Question.js new file mode 120000 index 0000000..f9185e0 --- /dev/null +++ b/classes/Question.js @@ -0,0 +1 @@ +../build/classes/Question.js \ No newline at end of file diff --git a/demo.js b/demo.js index 03c9ce7..b83c0de 100644 --- a/demo.js +++ b/demo.js @@ -1,12 +1,8 @@ -var Bot = require('./index'); -var Message = require('./lib/classes/Message'); -var Question = require('./lib/classes/Question'); +var Bot = require('telegram-api'); -process.on('uncaughtException', function (err) { - console.error((new Date()).toUTCString() + ' uncaughtException:', err.message); - console.error(err.stack); - process.exit(1); -}); +// only require the message types you need, more coming soon! +var Message = require('telegram-api/types/Message'); +var Question = require('telegram-api/types/Question'); var smartBot = new Bot({ token: '121143906:AAE6pcpBoARNZZjr3fUpvKuLInJ5Eee5Ajk' @@ -17,16 +13,24 @@ smartBot.start().then(() => { console.log(smartBot.info); }); -// You can use regular expressions, too -smartBot.get('Hi', function(update) { +// Create a new question +// answers is a keyboard layout as defined in Telegram API +// we're going to reuse this by modifying it's target +const question = new Question() + .text('How should I greet you?') + .answers([['Hey'], ['Hello, Sir'], ['Yo bro']]); + +// Called when a message starting with Hi is received +// You can use Regular Expressions, too +// update is an Update object as defined in Telegram API +smartBot.get('Hi', update => { const message = update.message; const id = message.chat.id; - var question = new Question().to(id) - .text('How should I greet you?') - .answers([['Hi'], ['Hello, Sir'], ['Yo bro']]) - .reply(message.message_id); + question.to(id).reply(message.message_id); + // Send the question, returns a promise, resolves on valid answer, + // rejects in case of an invalid answer smartBot.send(question).then(answer => { const msg = new Message().to(id).text('Your answer: ' + answer); smartBot.send(msg); @@ -36,19 +40,16 @@ smartBot.get('Hi', function(update) { }); }); -// Commands are in format `/command` or `/command@botusername` in groups +// Commands are in the format `/command` or `/command@botusername` in groups +const test = new Message().text('Test Command'); smartBot.command('test', update => { const message = update.message; const id = message.chat.id; - // options object => Telegram API - smartBot.send(new Message({ - chat_id: id, - text: 'Test Command' - })); + smartBot.send(test.to(id)); }); +const hello = new Message().text('Hello'); smartBot.command('start', update => { - // chainable methods => easier - smartBot.send(new Message().to(update.message.chat.id).text('Hello')); + smartBot.send(hello.to(update.message.chat.id)); }); diff --git a/esdoc.json b/esdoc.json new file mode 100644 index 0000000..a0b93b5 --- /dev/null +++ b/esdoc.json @@ -0,0 +1,4 @@ +{ + "src": "./lib", + "destination": "./docs" +} diff --git a/index.js b/index.js index 75cdce6..8ae3093 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,2 @@ -require('babel/register'); -module.exports = require('./lib/index'); +require('babel/polyfill'); +module.exports = require('./build/'); diff --git a/lib/.eslintrc b/lib/.eslintrc index 33fe593..1d47ccc 100644 --- a/lib/.eslintrc +++ b/lib/.eslintrc @@ -1,5 +1,59 @@ { + "env": { + "es6": true, + "browser": true + }, + "ecmaFeatures": { + "modules": true + }, + "globals": { + "exports": true, + "require": true, + "ViewHelpers": true, + "is": true, + "Components": true, + "XPCOMUtils": true, + "EventEmitter": true, + "add_task": true, + "info": true, + "createHost": true, + "promiseTab": true, + "ok": true, + "TEST_URI_ROOT": true, + "TargetFactory": true, + "gBrowser": true, + "gDevTools": true + }, "rules": { + "comma-dangle": [2, "never"], + "no-underscore-dangle": 0, + "no-cond-assign": 0, + "no-undef": 0, + "no-console": 0, + "no-reserved-keys": 2, + "valid-jsdoc": [2, { + "requireReturn": false, + "requireParamDescription": false, + "requireReturnDescription": false + }], + "max-len": [1, 80], + "no-use-before-define": 0, + "no-self-compare": 1, + "no-sequences": 0, + "radix": 2, + "wrap-iife": 2, + "indent": [2, 2], + "brace-style": [2, "1tbs"], + "comma-style": [2, "last"], + "no-lonely-if": 2, + "no-multiple-empty-lines": [2, {"max": 2}], + "quotes": 0, + "space-after-keywords": [2, "always"], + "space-before-blocks": [2, "always"], + "space-infix-ops": [2, { "int32Hint": false }], + "strict": 0, + "global-strict": 0, + "no-new": 0, "camelcase": 0 } } diff --git a/lib/api.js b/lib/api.js index 005c368..72c4222 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,8 +1,22 @@ // API methods import fetch from './fetch'; -export default function API(token) { - this.token = token; +/** + * API class, has a function for each method of the Telegram API which take + * an object argument, and send request to the API server + * + * Methods: getMe, sendMessage, forwardMessage, sendPhoto, sendAudio, + * sendDocument, sendSticker, sendVideo, sendLocation, sendChatAction, + * getUserProfilePhotos, getUpdates + */ +export default class API { + /** + * Create a new api object with the given token + * @param {string} token + */ + constructor(token) { + this.token = token; + } } API.prototype.request = function request(method, data) { diff --git a/lib/classes/Keyboard.js b/lib/classes/Keyboard.js deleted file mode 100644 index e7b9590..0000000 --- a/lib/classes/Keyboard.js +++ /dev/null @@ -1,61 +0,0 @@ -export default class Keyboard { - constructor(message, options = {}) { - this.message = message; - this.replyMarkup = options; - } - - keys(keys) { - this.setProperties({ - keyboard: keys, - hide_keyboard: false - }); - return this; - } - - force(enable = true) { - this.setProperties({ - force_keyboard: enable - }); - return this; - } - - resize(enable = true) { - this.setProperties({ - resize: enable - }); - return this; - } - - oneTime(enable = true) { - this.setProperties({ - one_time_keyboard: enable - }); - return this; - } - - selective(enable = true) { - this.setProperties({ - selective: enable - }); - return this; - } - - hide() { - this.replyMarkup = { - hide_keyboard: true - }; - - return this; - } - - get replyMarkup() { - return JSON.parse(this.message.params.reply_markup); - } - set replyMarkup(json) { - this.message.params.reply_markup = JSON.stringify(json); - } - - setProperties(object) { - this.replyMarkup = Object.assign(this.replyMarkup, object); - } -} diff --git a/lib/classes/Message.js b/lib/classes/Message.js deleted file mode 100644 index 6b75e16..0000000 --- a/lib/classes/Message.js +++ /dev/null @@ -1,77 +0,0 @@ -import {EventEmitter} from 'events'; -import Keyboard from './Keyboard'; - -export default class Message extends EventEmitter { - constructor(options = {}) { - super(); - - this.params = options; - } - - to(chat) { - this.params.chat_id = chat; - return this; - } - - text(text) { - this.params.text = text; - return this; - } - - reply(chat) { - this.params.reply_to_message_id = chat; - return this; - } - - keyboard(options) { - let params; - - if (this._keyboard && !options) { - return this._keyboard; - } - - if (this._keyboard) { - params = Object.assign(this._keyboard.replyMarkup, options); - } else { - params = options; - } - - this._keyboard = new Keyboard(this, params); - return this._keyboard; - } - - send(bot) { - let messageId; - - return new Promise(resolve => { - bot.api.sendMessage(this.params).then(response => { - messageId = response.result.message_id; - this.emit('message:sent', response); - }); - - if (this.keyboard().replyMarkup.one_time_keyboard) { - this.keyboard().replyMarkup = ''; - } - - const chat = this.params.chat_id; - bot.on('update', function listener(result) { - const update = result.find(({message}) => { - // if in a group, there will be a reply to this message - if (chat < 0) { - return message.chat.id === chat && - message.reply_to_message.message_id === messageId; - } else { - return message.chat.id === chat; - } - }); - - if (update) { - resolve(update); - this.emit('message:answer', update); - - bot.removeListener('update', listener); - } - }); - }); - } -} diff --git a/lib/classes/Question.js b/lib/classes/Question.js deleted file mode 100644 index a982dbe..0000000 --- a/lib/classes/Question.js +++ /dev/null @@ -1,43 +0,0 @@ -import Message from './Message'; - -export default class Question extends Message { - constructor(options = {}) { - super(options); - - this.keyboard().force().oneTime().selective(); - } - - answers(answers) { - this.answers = answers; - this.keyboard().keys(answers); - return this; - } - - send(bot) { - const answers = this.answers; - - return new Promise((resolve, reject) => { - super.send(bot).then(update => { - const message = update.message; - let answer; - - answers.forEach(function find(a) { - if (Array.isArray(a)) { - a.forEach(find); - } - if (a === message.text) { - answer = a; - } - }); - - if (answer) { - resolve(answer, update); - this.emit('question:answer', answer, update); - } else { - reject(update); - this.emit('question:invalid', update); - } - }); - }); - } -} diff --git a/lib/fetch.js b/lib/fetch.js index 913b326..0642ae4 100644 --- a/lib/fetch.js +++ b/lib/fetch.js @@ -1,7 +1,7 @@ import https from 'https'; import qs from 'qs'; -export default function(path, data) { +export default function fetch(path, data) { const post = qs.stringify(data); return new Promise((resolve, reject) => { diff --git a/lib/index.js b/lib/index.js index fa02acd..036982f 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,4 @@ +import 'babel/polyfill'; import API from './api'; import {EventEmitter} from 'events'; @@ -9,11 +10,23 @@ const DEFAULTS = { } }; -export default class Bot { +/** + * Bot class used to connect to a new bot + * Bots have an api property which gives access to all Telegram API methods, + * see API class + */ +export default class Bot extends EventEmitter { + /** + * Create and connect to a new bot + * @param {object} options Bot properties. + */ constructor(options = {update: {}}) { + super(); + if (!options.token) { throw new Error('Token cannot be empty'); } + this.token = options.token; this.update = Object.assign(options.update || {}, DEFAULTS.update); @@ -26,6 +39,13 @@ export default class Bot { this._userEvents = []; } + /** + * Gets information about the bot and then starts polling updates from API + * Emits an `update` event after polling with the response from server + * Returns a promise which is resolved after the bot information is received + * and set to it's `info` property i.e. bot.info + * @return {promise} A promise which is resolved with the response of getMe + */ start() { let poll = function() { this.api.getUpdates(this.update).then(response => { @@ -47,13 +67,13 @@ export default class Bot { this.emit('update', response.result); result.forEach(res => { let text = res.message.text; - if (text.indexOf('/') === 0) { - // Commands are sent in format /command@botusername format + if (text.startsWith('/')) { + // Commands are sent in /command@botusername format in groups const regex = new RegExp(`@${this.info.username}$`); text = text.replace(regex, ''); } - let ev = this._userEvents.find(({message}) => message.test(text)); + let ev = this._userEvents.find(({pattern}) => pattern.test(text)); ev.listener(res); }); }); @@ -65,26 +85,50 @@ export default class Bot { }); } - get(message, listener) { - if (typeof message === 'string') { - message = new RegExp(`^${message}`); + /** + * Listens on specific message matching the pattern which can be an string + * or a regexp. + * @param {string/regex} pattern + * @param {function} listener function to call when a message matching the + * pattern is found, gets the Update + * In case of string, the message should start + * with the string i.e. /^yourString/ + * @return {object} returns the bot object + */ + get(pattern, listener) { + if (typeof pattern === 'string') { + pattern = new RegExp(`^${pattern}`); } this._userEvents.push({ - message, listener + pattern, listener }); + + return this; } + /** + * Listens on a command + * @param {string} cmd the command string, should not include slash (/) + * @param {function} listener function to call when the command is received, + * gets the update + * @return {object} returns the bot object + */ command(cmd, listener) { this._userEvents.push({ - message: new RegExp(`/${cmd}`), + pattern: new RegExp(`/${cmd}`), listener }); + + return this; } + /** + * Sends the message provided + * @param {object} message The message to send. Gets it's send method called + * @return {unknown} returns the result of calling message's send method + */ send(message) { return message.send(this); } } - -Bot.prototype = Object.assign(Bot.prototype, EventEmitter.prototype); diff --git a/lib/types/Base.js b/lib/types/Base.js new file mode 100644 index 0000000..f31a596 --- /dev/null +++ b/lib/types/Base.js @@ -0,0 +1,35 @@ +import {EventEmitter} from 'events'; + +/** + * Base class of all classes + */ +export default class Base extends EventEmitter { + constructor() { + super(); + this.properties = {}; + } + + /** + * Returns properties of the object + * @return {object} properties of object + */ + getProperties() { + return this.properties; + } + + /** + * Set properties of the object + * @param {object} object properties to set + * @param {boolean} extend A boolean indicating if the properties should be + * extended by the object provided (Object.assign) + * or properties should be replaced by the object + * defaults to true + * @return {object} returns the properties (same as getProperties) + */ + setProperties(object, extend = true) { + this.properties = extend ? Object.assign(this.properties, object) + : object; + + return this.getProperties(); + } +} diff --git a/lib/types/Keyboard.js b/lib/types/Keyboard.js new file mode 100644 index 0000000..1bcce2e --- /dev/null +++ b/lib/types/Keyboard.js @@ -0,0 +1,85 @@ +import Base from './Base'; + +/** + * Keyboard class, used to configure keyboards for messages. + * You should pass your instance of this class to message.keyboard() method + */ +export default class Keyboard extends Base { + /** + * Create a new keyboard + * @param {object} properties Keyboard properties, as defined by Telegram API + * See ReplyKeyboardMarkup, ReplyKeyboardHide, + * ForceReply + */ + constructor(properties = {}) { + super(); + + this.properties = properties; + } + + /** + * Set the keyboard property of reply_markup + * @param {array} keys An array of arrays, with the format of + * Column Column + * Row [['TopLeft', 'TopRight'], + * Row ['BottomLeft', 'BottomRight']] + * @return {object} returns the keyboard object + */ + keys(keys) { + this.properties.keyboard = keys; + this.properties.hide_keyboard = false; + return this; + } + + /** + * Set force_keyboard property of reply_markup + * @param {boolean} enable value of force_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + force(enable = true) { + this.properties.force_keyboard = enable; + return this; + } + + /** + * Set resize_keyboard property of reply_markup + * @param {boolean} enable value of resize_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + resize(enable = true) { + this.properties.resize_keyboard = enable; + return this; + } + + /** + * Set force_keyboard property of reply_markup + * @param {boolean} enable value of force_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + oneTime(enable = true) { + this.properties.one_time_keyboard = enable; + return this; + } + + /** + * Set selective property of reply_markup + * @param {boolean} enable value of force_keyboard, defaults to true + * @return {object} returns the keyboard object + */ + selective(enable = true) { + this.properties.selective = enable; + return this; + } + + /** + * Set hide_keyboard property of reply_markup to true + * @return {object} returns the keyboard object + */ + hide() { + this.properties = { + hide_keyboard: true + }; + + return this; + } +} diff --git a/lib/types/Message.js b/lib/types/Message.js new file mode 100644 index 0000000..1e1a455 --- /dev/null +++ b/lib/types/Message.js @@ -0,0 +1,110 @@ +import Base from './Base'; + +/** + * Message class, used to send message to a chat + */ +export default class Message extends Base { + /** + * Create a new message + * @param {object} properties Message properties, as defined by Telegram API + */ + constructor(properties = {}) { + super(); + + this.properties = properties; + this._keyboard = new Base(); + } + + /** + * Set chat_id of the message + * @param {number} chat + * @return {object} returns the message object + */ + to(chat) { + this.properties.chat_id = chat; + return this; + } + + /** + * Set text of the message + * @param {string} text Message's content + * @return {object} returns the message object + */ + text(text) { + this.properties.text = text; + return this; + } + + /** + * Set reply_to_message_id of the message + * @param {number} id message_id of the message to reply to + * @return {object} returns the message object + */ + reply(id) { + this.properties.reply_to_message_id = id; + return this; + } + + /** + * Sets keyboard of the message + * The value of reply_markup is set to the sanitized keyboard properties + * i.e. reply_markup = JSON.stringify(kb.getProperties()) + * @param {object} kb A Keyboard instance + * @return {object} returns the message object + */ + keyboard(kb) { + this._keyboard = kb; + return this; + } + + /** + * Sends the message, you should only use this method yourself if + * you are extending this class. Normally you should call bot.send(message) + * + * Events: message:sent => Emitted after sending the message to API, gets the + * API's response + * + * message:answer => Emitted when your message gets an answer from + * the contact (reply in case of groups) + * gets the Update object containing message + * + * @param {object} bot + * @return {promise} returns a promise, resolved with message:answer + */ + send(bot) { + let messageId; + const reply_markup = JSON.stringify(this._keyboard.getProperties()); + this.properties.reply_markup = reply_markup; + + return new Promise(resolve => { + bot.api.sendMessage(this.properties).then(response => { + messageId = response.result.message_id; + this.emit('message:sent', response); + }); + + if (this._keyboard.one_time_keyboard) { + this._keyboard.replyMarkup = ''; + } + + const chat = this.properties.chat_id; + bot.on('update', function listener(result) { + const update = result.find(({message}) => { + // if in a group, there will be a reply to this message + if (chat < 0) { + return message.chat.id === chat && + message.reply_to_message.message_id === messageId; + } else { + return message.chat.id === chat; + } + }); + + if (update) { + resolve(update); + this.emit('message:answer', update); + + bot.removeListener('update', listener); + } + }); + }); + } +} diff --git a/lib/types/Question.js b/lib/types/Question.js new file mode 100644 index 0000000..965c30f --- /dev/null +++ b/lib/types/Question.js @@ -0,0 +1,71 @@ +import Message from './Message'; +import Keyboard from './Keyboard'; + +/** + * Question class, extends Message + * Sends a message, shows a keyboard with the answers provided, and validates + * the answer + */ +export default class Question extends Message { + /** + * Create a new question + * @param {object} options Options, same as Message, plus `answers` which + * is a keyboard layout, see Keyboard#keys + */ + constructor(options = {}) { + super(options); + + let kb = new Keyboard().force().oneTime().selective() + .keys(this.properties.answers); + this.keyboard(kb); + } + + /** + * Sets answers of the question. This is passed to Keyboard#keys, and then + * used to validate the answer given + * @param {array} answers Array of arrays of strings, same as Keyboard#keys + * @return {object} returns the question object + */ + answers(answers) { + this.answers = answers; + this._keyboard.keys(answers); + return this; + } + + /** + * Sends the question (same as Message#send), and validates the answer given + * if the answer is one of the defined answers, resolves, else rejects + * You should not manually use this method unless you're extending this class + * You should instead use bot.send(question); + * @param {object} bot + * @return {promise} A promise which is resolved in case of valid answer, and + * rejected in case of invalid answer + */ + send(bot) { + const answers = this.answers; + + return new Promise((resolve, reject) => { + super.send(bot).then(update => { + const message = update.message; + let answer; + + answers.forEach(function find(a) { + if (Array.isArray(a)) { + a.forEach(find); + } + if (a === message.text) { + answer = a; + } + }); + + if (answer) { + resolve(answer, update); + this.emit('question:answer', answer, update); + } else { + reject(update); + this.emit('question:invalid', update); + } + }); + }); + } +} diff --git a/lib/utils.js b/lib/utils.js deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 27a68ed..8d12491 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-api", - "version": "0.1.1", + "version": "0.2.1", "description": "Control Telegram bots easily using the new Telegram API", "main": "index.js", "scripts": { @@ -21,9 +21,21 @@ "bugs": { "url": "https://github.com/mdibaiee/node-telegram-api/issues" }, + "directories": { + "lib": "lib", + "doc": "docs" + }, + "engines": { + "node": ">=0.12.0" + }, "homepage": "https://github.com/mdibaiee/node-telegram-api", "dependencies": { - "babel": "^5.6.8", + "babel": "^5.6.14", + "grunt": "^0.4.5", + "grunt-babel": "^5.0.1", + "grunt-contrib-symlink": "^0.3.0", + "grunt-contrib-watch": "^0.6.1", + "grunt-eslint": "^16.0.0", "qs": "^3.1.0" } } diff --git a/types/Base.js b/types/Base.js new file mode 120000 index 0000000..bb923bf --- /dev/null +++ b/types/Base.js @@ -0,0 +1 @@ +../build/types/Base.js \ No newline at end of file diff --git a/types/Keyboard.js b/types/Keyboard.js new file mode 120000 index 0000000..18d709c --- /dev/null +++ b/types/Keyboard.js @@ -0,0 +1 @@ +../build/types/Keyboard.js \ No newline at end of file diff --git a/types/Message.js b/types/Message.js new file mode 120000 index 0000000..d7fc2c6 --- /dev/null +++ b/types/Message.js @@ -0,0 +1 @@ +../build/types/Message.js \ No newline at end of file diff --git a/types/Question.js b/types/Question.js new file mode 120000 index 0000000..1d74df5 --- /dev/null +++ b/types/Question.js @@ -0,0 +1 @@ +../build/types/Question.js \ No newline at end of file