From bd4f0ed027c13edcc205df43b980a3799c7d2100 Mon Sep 17 00:00:00 2001 From: Mahdi Dibaiee Date: Sun, 5 Jul 2015 18:06:27 +0430 Subject: [PATCH] =?UTF-8?q?Add=20Argument=20Parser=20=F0=9F=92=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/functions/api.js | 49 ++++++++++ build/functions/argument-parser.js | 139 +++++++++++++++++++++++++++++ build/functions/fetch.js | 49 ++++++++++ build/functions/poll.js | 19 ++++ build/functions/webhook.js | 42 +++++++++ build/index.js | 39 +++++--- demo.js | 4 +- lib/{ => functions}/api.js | 0 lib/functions/argument-parser.js | 98 ++++++++++++++++++++ lib/{ => functions}/fetch.js | 0 lib/{ => functions}/poll.js | 0 lib/{ => functions}/webhook.js | 0 lib/index.js | 20 +++-- package.json | 2 +- 14 files changed, 440 insertions(+), 21 deletions(-) create mode 100644 build/functions/api.js create mode 100644 build/functions/argument-parser.js create mode 100644 build/functions/fetch.js create mode 100644 build/functions/poll.js create mode 100644 build/functions/webhook.js rename lib/{ => functions}/api.js (100%) create mode 100644 lib/functions/argument-parser.js rename lib/{ => functions}/fetch.js (100%) rename lib/{ => functions}/poll.js (100%) rename lib/{ => functions}/webhook.js (100%) diff --git a/build/functions/api.js b/build/functions/api.js new file mode 100644 index 0000000..0852b14 --- /dev/null +++ b/build/functions/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', 'setWebhook']; + +methods.forEach(function (method) { + API.prototype[method] = function (data) { + return this.request(method, data); + }; +}); +module.exports = exports['default']; diff --git a/build/functions/argument-parser.js b/build/functions/argument-parser.js new file mode 100644 index 0000000..9c5d9e0 --- /dev/null +++ b/build/functions/argument-parser.js @@ -0,0 +1,139 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); + +var _slicedToArray = (function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i['return']) _i['return'](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError('Invalid attempt to destructure non-iterable instance'); } }; })(); + +exports['default'] = argumentParser; +var FORMAT_REQUIRED = /<(\W*)(\w+)\|?(\w+)?>/g; +var FORMAT_OPTIONAL = /\[(\W*)(\w+)\|?(\w+)?\]/g; +var FORMAT_REST = /\.{3}(\w+)/g; + +var ESCAPABLE = '.^$*+?()[{\\|}]'.split(''); + +/** + * Parses a message for arguments, based on format + * + * The format option may include '' and '[optionalParam]' and + * '...[restParam]' + * indicates a required, single-word argument + * [optionalParam] indicates an optinal, single-word argument + * ...[restParam] indicates a multi-word argument which records until end + * + * You can define a type for your arguments using pipe | sign, like this: + * [count|number] + * Supported Types are: number and word, defaults to word + * + * Example: + * format: ' [count|number] ...text' + * string 1: 'Someone Hey, wassup' + * {name: 'Someone', + * count: undefined, + * text: 'Hey, wassup'} + * + * string 2: 'Someone 5 Hey, wassup' + * {name: 'Someone', + * count: 5, + * text: 'Hey, wassup'} + * @param {string} format Format, as described above + * @param {string} string The message to parse + * @return {object} Parsed arguments + */ + +function argumentParser(format, string) { + string = string.replace(/[^\s]+/, ''); + format = format.replace(/[^\s]+/, ''); + var indexes = []; + + format = format.replace(/\s/g, '\\s*'); + format = format.replace(FORMAT_REQUIRED, function (f, symbols, arg, type, offset) { + if (type === undefined) type = 'word'; + + indexes.push({ arg: arg, offset: offset }); + return (escape(symbols) + getFormat(type, 'required')).trim(); + }); + format = format.replace(FORMAT_OPTIONAL, function (f, symbols, arg, type, offset) { + if (type === undefined) type = 'word'; + + indexes.push({ arg: arg, offset: offset }); + return (escape(symbols, '?') + getFormat(type, 'optional')).trim(); + }); + format = format.replace(FORMAT_REST, function (full, arg, offset) { + indexes.push({ offset: offset, arg: arg }); + return getFormat(null, 'rest'); + }); + + indexes = indexes.sort(function (a, b) { + return a.offset < b.offset ? -1 : 1; + }); + + console.log(format); + var regex = new RegExp(format); + + var matched = regex.exec(string).slice(1); + + var object = {}; + var _iteratorNormalCompletion = true; + var _didIteratorError = false; + var _iteratorError = undefined; + + try { + for (var _iterator = matched.entries()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { + var _step$value = _slicedToArray(_step.value, 2); + + var index = _step$value[0]; + var match = _step$value[1]; + + var argument = indexes[index]; + + object[argument.arg] = match; + } + } catch (err) { + _didIteratorError = true; + _iteratorError = err; + } finally { + try { + if (!_iteratorNormalCompletion && _iterator['return']) { + _iterator['return'](); + } + } finally { + if (_didIteratorError) { + throw _iteratorError; + } + } + } + + return object; +} + +function escape(symbols) { + var append = arguments[1] === undefined ? '' : arguments[1]; + + return symbols.split('').map(function (symbol) { + return (ESCAPABLE.indexOf(symbol) ? '\\' + symbol : symbol) + append; + }).join(''); +} + +var TYPES = { + 'number': '\\d', + 'word': '\\w' +}; + +function getFormat() { + var type = arguments[0] === undefined ? 'word' : arguments[0]; + var param = arguments[1] === undefined ? 'required' : arguments[1]; + + var t = TYPES[type]; + + switch (param) { + case 'required': + return '(' + t + '+)'; + case 'optional': + return '(' + t + '+)?'; + case 'rest': + return '(.*)'; + } +} +module.exports = exports['default']; diff --git a/build/functions/fetch.js b/build/functions/fetch.js new file mode 100644 index 0000000..97275ee --- /dev/null +++ b/build/functions/fetch.js @@ -0,0 +1,49 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports['default'] = fetch; +exports.getBody = getBody; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _restler = require('restler'); + +var _restler2 = _interopRequireDefault(_restler); + +function fetch(path) { + var data = arguments[1] === undefined ? {} : arguments[1]; + + return new Promise(function (resolve, reject) { + var method = Object.keys(data).length ? 'POST' : 'GET'; + var multipart = method === 'POST' ? true : false; + + _restler2['default'].request('https://api.telegram.org/bot' + path, { + data: data, method: method, multipart: multipart + }).on('complete', function (response) { + try { + var json = JSON.parse(response); + resolve(json); + } catch (e) { + reject(e); + } + }); + }); +} + +function getBody(stream) { + var data = ''; + + return new Promise(function (resolve, reject) { + stream.on('data', function (chunk) { + data += chunk; + }); + + stream.on('end', function () { + resolve(data); + }); + + stream.on('error', reject); + }); +} diff --git a/build/functions/poll.js b/build/functions/poll.js new file mode 100644 index 0000000..89e41a7 --- /dev/null +++ b/build/functions/poll.js @@ -0,0 +1,19 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports['default'] = poll; + +function poll(bot) { + return bot.api.getUpdates(bot.update).then(function (response) { + if (!response.result.length) { + return poll(bot); + } + bot.emit('update', response.result); + + return poll(bot); + }); +} + +module.exports = exports['default']; diff --git a/build/functions/webhook.js b/build/functions/webhook.js new file mode 100644 index 0000000..c00c59f --- /dev/null +++ b/build/functions/webhook.js @@ -0,0 +1,42 @@ +'use strict'; + +Object.defineProperty(exports, '__esModule', { + value: true +}); +exports['default'] = webhook; + +function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } + +var _http = require('http'); + +var _http2 = _interopRequireDefault(_http); + +var _qs = require('qs'); + +var _qs2 = _interopRequireDefault(_qs); + +var _fetch = require('./fetch'); + +var DEFAULTS = { + server: {}, + port: 443 +}; + +function webhook(options, bot) { + if (options === undefined) options = {}; + + options = Object.assign(DEFAULTS, options); + + return bot.api.setWebhook(options.url).then(function () { + + _http2['default'].createServer(options.server, function (req, res) { + return (0, _fetch.getBody)(req).then(function (data) { + bot.emit('update', _qs2['default'].parse(data).result); + + res.end('OK'); + }); + }).listen(options.port); + }); +} + +module.exports = exports['default']; diff --git a/build/index.js b/build/index.js index 736862c..76d026e 100644 --- a/build/index.js +++ b/build/index.js @@ -14,24 +14,28 @@ function _classCallCheck(instance, Constructor) { if (!(instance instanceof Cons 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 _api = require('./api'); +var _functionsApi = require('./functions/api'); -var _api2 = _interopRequireDefault(_api); +var _functionsApi2 = _interopRequireDefault(_functionsApi); -var _webhook = require('./webhook'); +var _functionsWebhook = require('./functions/webhook'); -var _webhook2 = _interopRequireDefault(_webhook); +var _functionsWebhook2 = _interopRequireDefault(_functionsWebhook); -var _poll = require('./poll'); +var _functionsPoll = require('./functions/poll'); -var _poll2 = _interopRequireDefault(_poll); +var _functionsPoll2 = _interopRequireDefault(_functionsPoll); + +var _functionsArgumentParser = require('./functions/argument-parser'); + +var _functionsArgumentParser2 = _interopRequireDefault(_functionsArgumentParser); var _events = require('events'); var DEFAULTS = { update: { offset: 0, - timeout: 0.5, + timeout: 20, limit: 100 } }; @@ -62,7 +66,7 @@ var Bot = (function (_EventEmitter) { this.token = options.token; this.update = Object.assign(options.update || {}, DEFAULTS.update); - this.api = new _api2['default'](this.token); + this.api = new _functionsApi2['default'](this.token); this.msg = {}; @@ -99,7 +103,7 @@ var Bot = (function (_EventEmitter) { var _this = this; if (hook) { - return (0, _webhook2['default'])(hook, this); + return (0, _functionsWebhook2['default'])(hook, this); } return this.api.getMe().then(function (response) { _this.info = response.result; @@ -107,9 +111,9 @@ var Bot = (function (_EventEmitter) { _this.on('update', _this._update); if (hook) { - return (0, _webhook2['default'])(hook, _this); + return (0, _functionsWebhook2['default'])(hook, _this); } else { - return (0, _poll2['default'])(_this); + return (0, _functionsPoll2['default'])(_this); } }); } @@ -142,14 +146,19 @@ var Bot = (function (_EventEmitter) { /** * Listens on a command - * @param {string} cmd the command string, should not include slash (/) + * @param {string} command 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) { + value: function command(_command, listener) { + var regex = /[^\s]+/; + + var cmd = _command.match(regex)[0]; + this._userEvents.push({ pattern: new RegExp('^/' + cmd), + parse: _functionsArgumentParser2['default'].bind(null, _command), listener: listener }); @@ -207,6 +216,10 @@ var Bot = (function (_EventEmitter) { return; } + if (ev.parse) { + res.message.args = ev.parse(res.message.text); + } + ev.listener(res.message); }); } diff --git a/demo.js b/demo.js index d5e047f..5420883 100644 --- a/demo.js +++ b/demo.js @@ -43,6 +43,6 @@ bot.command('start', message => { const test = new Message().text('Test Command'); -bot.command('test', message => { - bot.send(test.to(message.chat.id).text(message.text)); +bot.command('test [count|number] ...rest', message => { + bot.send(test.to(message.chat.id).text(message.args.subject)); }); diff --git a/lib/api.js b/lib/functions/api.js similarity index 100% rename from lib/api.js rename to lib/functions/api.js diff --git a/lib/functions/argument-parser.js b/lib/functions/argument-parser.js new file mode 100644 index 0000000..39e2006 --- /dev/null +++ b/lib/functions/argument-parser.js @@ -0,0 +1,98 @@ +const FORMAT_REQUIRED = /<(\W*)(\w+)\|?(\w+)?>/g; +const FORMAT_OPTIONAL = /\[(\W*)(\w+)\|?(\w+)?\]/g; +const FORMAT_REST = /\.{3}(\w+)/g; + +const ESCAPABLE = '.^$*+?()[{\\|}]'.split(''); + +/** + * Parses a message for arguments, based on format + * + * The format option may include '' and '[optionalParam]' and + * '...[restParam]' + * indicates a required, single-word argument + * [optionalParam] indicates an optinal, single-word argument + * ...[restParam] indicates a multi-word argument which records until end + * + * You can define a type for your arguments using pipe | sign, like this: + * [count|number] + * Supported Types are: number and word, defaults to word + * + * Example: + * format: ' [count|number] ...text' + * string 1: 'Someone Hey, wassup' + * {name: 'Someone', + * count: undefined, + * text: 'Hey, wassup'} + * + * string 2: 'Someone 5 Hey, wassup' + * {name: 'Someone', + * count: 5, + * text: 'Hey, wassup'} + * @param {string} format Format, as described above + * @param {string} string The message to parse + * @return {object} Parsed arguments + */ +export default function argumentParser(format, string) { + string = string.replace(/[^\s]+/, ''); + format = format.replace(/[^\s]+/, ''); + let indexes = []; + + format = format.replace(/\s/g, '\\s*'); + format = format.replace(FORMAT_REQUIRED, + (f, symbols, arg, type = 'word', offset) => { + indexes.push({arg, offset}); + return (escape(symbols) + getFormat(type, 'required')).trim(); + }); + format = format.replace(FORMAT_OPTIONAL, + (f, symbols, arg, type = 'word', offset) => { + indexes.push({arg, offset}); + return (escape(symbols, '?') + getFormat(type, 'optional')).trim(); + }); + format = format.replace(FORMAT_REST, (full, arg, offset) => { + indexes.push({offset, arg}); + return getFormat(null, 'rest'); + }); + + indexes = indexes.sort((a, b) => { + return a.offset < b.offset ? -1 : 1; + }); + + console.log(format); + const regex = new RegExp(format); + + const matched = regex.exec(string).slice(1); + + const object = {}; + for (let [index, match] of matched.entries()) { + const argument = indexes[index]; + + object[argument.arg] = match; + } + + return object; +} + +function escape(symbols, append = '') { + return symbols.split('').map(symbol => { + return (ESCAPABLE.indexOf(symbol) ? `\\${symbol}` : symbol) + append; + }).join(''); +} + + +const TYPES = { + 'number': '\\d', + 'word': '\\w' +}; + +function getFormat(type = 'word', param = 'required') { + const t = TYPES[type]; + + switch (param) { + case 'required': + return `(${t}+)`; + case 'optional': + return `(${t}+)?`; + case 'rest': + return `(.*)`; + } +} diff --git a/lib/fetch.js b/lib/functions/fetch.js similarity index 100% rename from lib/fetch.js rename to lib/functions/fetch.js diff --git a/lib/poll.js b/lib/functions/poll.js similarity index 100% rename from lib/poll.js rename to lib/functions/poll.js diff --git a/lib/webhook.js b/lib/functions/webhook.js similarity index 100% rename from lib/webhook.js rename to lib/functions/webhook.js diff --git a/lib/index.js b/lib/index.js index 5867021..c5871c3 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,6 +1,7 @@ -import API from './api'; -import webhook from './webhook'; -import poll from './poll'; +import API from './functions/api'; +import webhook from './functions/webhook'; +import poll from './functions/poll'; +import argumentParser from './functions/argument-parser'; import {EventEmitter} from 'events'; const DEFAULTS = { @@ -100,14 +101,19 @@ export default class Bot extends EventEmitter { /** * Listens on a command - * @param {string} cmd the command string, should not include slash (/) + * @param {string} command 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) { + command(command, listener) { + const regex = /[^\s]+/; + + const cmd = command.match(regex)[0]; + this._userEvents.push({ pattern: new RegExp(`^/${cmd}`), + parse: argumentParser.bind(null, command), listener }); @@ -156,6 +162,10 @@ export default class Bot extends EventEmitter { return; } + if (ev.parse) { + res.message.args = ev.parse(res.message.text); + } + ev.listener(res.message); }); } diff --git a/package.json b/package.json index cee8006..125b3cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "telegram-api", - "version": "0.4.64", + "version": "0.4.65", "description": "Control Telegram bots easily using the new Telegram API", "main": "index.js", "scripts": {