Add support for WebHooks

This commit is contained in:
Mahdi Dibaiee 2015-06-30 02:50:34 +04:30
parent 4ac725ea58
commit b55fb3c7d6
12 changed files with 278 additions and 126 deletions

View File

@ -39,7 +39,7 @@ API.prototype.request = function request(method, data) {
return (0, _fetch2['default'])(this.token + '/' + method, data); return (0, _fetch2['default'])(this.token + '/' + method, data);
}; };
var methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto', 'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo', 'sendLocation', 'sendChatAction', 'getUserProfilePhotos', 'getUpdates']; var methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto', 'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo', 'sendLocation', 'sendChatAction', 'getUserProfilePhotos', 'getUpdates', 'setWebhook'];
methods.forEach(function (method) { methods.forEach(function (method) {
API.prototype[method] = function (data) { API.prototype[method] = function (data) {

View File

@ -4,6 +4,7 @@ Object.defineProperty(exports, '__esModule', {
value: true value: true
}); });
exports['default'] = fetch; exports['default'] = fetch;
exports.getBody = getBody;
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }
@ -19,8 +20,6 @@ function fetch(path, data) {
var post = _qs2['default'].stringify(data); var post = _qs2['default'].stringify(data);
return new Promise(function (resolve, reject) { return new Promise(function (resolve, reject) {
var res = '';
var req = _https2['default'].request({ var req = _https2['default'].request({
hostname: 'api.telegram.org', hostname: 'api.telegram.org',
method: data ? 'POST' : 'GET', method: data ? 'POST' : 'GET',
@ -29,19 +28,15 @@ function fetch(path, data) {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
}, function (response) { }, function (response) {
response.on('data', function (chunk) { return getBody(response).then(function (res) {
res += chunk;
});
response.on('end', function () {
try { try {
var json = JSON.parse(res); var json = JSON.parse(res);
resolve(json); resolve(json);
} catch (e) { } catch (e) {
reject(e); reject(e);
} }
})['catch'](reject);
}); });
}).on('error', reject);
if (post) { if (post) {
req.write(post); req.write(post);
@ -52,4 +47,18 @@ function fetch(path, data) {
}); });
} }
module.exports = exports['default']; 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);
});
}

View File

@ -20,6 +20,14 @@ var _api = require('./api');
var _api2 = _interopRequireDefault(_api); var _api2 = _interopRequireDefault(_api);
var _webhook = require('./webhook');
var _webhook2 = _interopRequireDefault(_webhook);
var _poll = require('./poll');
var _poll2 = _interopRequireDefault(_poll);
var _events = require('events'); var _events = require('events');
var DEFAULTS = { var DEFAULTS = {
@ -75,62 +83,38 @@ var Bot = (function (_EventEmitter) {
key: 'start', key: 'start',
/** /**
* Gets information about the bot and then starts polling updates from API * Gets information about the bot and then
* 1) starts polling updates from API
* 2) sets a webhook as defined by the first parameter and listens for updates
* Emits an `update` event after polling with the response from server * Emits an `update` event after polling with the response from server
* Returns a promise which is resolved after the bot information is received * Returns a promise which is resolved after the bot information is received
* and set to it's `info` property i.e. bot.info * and set to it's `info` property i.e. bot.info
*
* @param {object} hook An object containg options passed to webhook
* properties:
* - url: HTTPS url to listen on POST requests coming
* from the Telegram API
* - port: the port to listen to, defaults to 443
* - server: An object passed to https.createServer
*
* @return {promise} A promise which is resolved with the response of getMe * @return {promise} A promise which is resolved with the response of getMe
*/ */
value: function start() { value: function start(hook) {
var _this2 = this;
var poll = (function () {
var _this = this; var _this = this;
return this.api.getUpdates(this.update).then(function (response) { if (hook) {
var again = wait(_this.update.timeout * 1000).then(poll); return (0, _webhook2['default'])(hook, this);
var result = response.result;
if (!result.length) {
return again;
} }
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', 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);
});
if (!ev) {
return;
}
ev.listener(res.message);
});
return again;
});
}).bind(this);
return this.api.getMe().then(function (response) { return this.api.getMe().then(function (response) {
_this2.info = response.result; _this.info = response.result;
return poll();
_this.on('update', _this._update);
if (hook) {
return (0, _webhook2['default'])(hook, _this);
} else {
return (0, _poll2['default'])(_this);
}
}); });
} }
}, { }, {
@ -186,16 +170,42 @@ var Bot = (function (_EventEmitter) {
value: function send(message) { value: function send(message) {
return message.send(this)['catch'](console.error); return message.send(this)['catch'](console.error);
} }
}, {
key: '_update',
value: function _update(update) {
var _this2 = this;
if (!this.update.offset) {
var updateId = update[update.length - 1].update_id;
this.update.offset = updateId;
}
if (this.update) {
this.update.offset += 1;
}
update.forEach(function (res) {
var text = res.message.text;
if (text.startsWith('/')) {
// Commands are sent in /command@thisusername format in groups
var regex = new RegExp('@' + _this2.info.username + '$');
text = text.replace(regex, '');
}
var ev = _this2._userEvents.find(function (_ref) {
var pattern = _ref.pattern;
return pattern.test(text);
});
if (!ev) {
return;
}
ev.listener(res.message);
});
}
}]); }]);
return Bot; return Bot;
})(_events.EventEmitter); })(_events.EventEmitter);
exports['default'] = Bot; exports['default'] = Bot;
var wait = function wait(miliseconds) {
return new Promise(function (resolve) {
setTimeout(resolve, miliseconds);
});
};
module.exports = exports['default']; module.exports = exports['default'];

27
build/poll.js Normal file
View File

@ -0,0 +1,27 @@
'use strict';
Object.defineProperty(exports, '__esModule', {
value: true
});
exports['default'] = poll;
function poll(bot) {
return bot.api.getUpdates(bot.update).then(function (response) {
var again = wait(bot.update.timeout * 1000, bot).then(poll);
if (!response.result.length) {
return again;
}
bot.emit('update', response.result);
return again;
});
}
var wait = function wait(miliseconds, value) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve(value);
}, miliseconds);
});
};
module.exports = exports['default'];

42
build/webhook.js Normal file
View File

@ -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'];

View File

@ -5,7 +5,7 @@ var Message = require('telegram-api/types/Message');
var Question = require('telegram-api/types/Question'); var Question = require('telegram-api/types/Question');
var bot = new Bot({ var bot = new Bot({
token: 'YOUR_KEY' token: '121143906:AAE6pcpBoARNZZjr3fUpvKuLInJ5Eee5Ajk'
}); });
bot.start().catch(err => { bot.start().catch(err => {

View File

@ -26,7 +26,7 @@ API.prototype.request = function request(method, data) {
const methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto', const methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto',
'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo', 'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo',
'sendLocation', 'sendChatAction', 'getUserProfilePhotos', 'sendLocation', 'sendChatAction', 'getUserProfilePhotos',
'getUpdates']; 'getUpdates', 'setWebhook'];
methods.forEach(method => { methods.forEach(method => {
API.prototype[method] = function(data) { API.prototype[method] = function(data) {

View File

@ -5,8 +5,6 @@ export default function fetch(path, data) {
const post = qs.stringify(data); const post = qs.stringify(data);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let res = '';
const req = https.request({ const req = https.request({
hostname: 'api.telegram.org', hostname: 'api.telegram.org',
method: data ? 'POST' : 'GET', method: data ? 'POST' : 'GET',
@ -15,19 +13,15 @@ export default function fetch(path, data) {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded'
} }
}, response => { }, response => {
response.on('data', chunk => { return getBody(response).then(res => {
res += chunk;
});
response.on('end', () => {
try { try {
let json = JSON.parse(res); let json = JSON.parse(res);
resolve(json); resolve(json);
} catch(e) { } catch(e) {
reject(e); reject(e);
} }
}).catch(reject);
}); });
}).on('error', reject);
if (post) { if (post) {
req.write(post); req.write(post);
@ -37,3 +31,19 @@ export default function fetch(path, data) {
console.error('Error sending request', err); console.error('Error sending request', err);
}); });
} }
export function getBody(stream) {
let data = '';
return new Promise((resolve, reject) => {
stream.on('data', chunk => {
data += chunk;
});
stream.on('end', () => {
resolve(data);
});
stream.on('error', reject);
});
}

View File

@ -1,5 +1,7 @@
import 'babel/polyfill'; import 'babel/polyfill';
import API from './api'; import API from './api';
import webhook from './webhook';
import poll from './poll';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
const DEFAULTS = { const DEFAULTS = {
@ -44,55 +46,38 @@ export default class Bot extends EventEmitter {
} }
/** /**
* Gets information about the bot and then starts polling updates from API * Gets information about the bot and then
* 1) starts polling updates from API
* 2) sets a webhook as defined by the first parameter and listens for updates
* Emits an `update` event after polling with the response from server * Emits an `update` event after polling with the response from server
* Returns a promise which is resolved after the bot information is received * Returns a promise which is resolved after the bot information is received
* and set to it's `info` property i.e. bot.info * and set to it's `info` property i.e. bot.info
*
* @param {object} hook An object containg options passed to webhook
* properties:
* - url: HTTPS url to listen on POST requests coming
* from the Telegram API
* - port: the port to listen to, defaults to 443
* - server: An object passed to https.createServer
*
* @return {promise} A promise which is resolved with the response of getMe * @return {promise} A promise which is resolved with the response of getMe
*/ */
start() { start(hook) {
let poll = function() { if (hook) {
return this.api.getUpdates(this.update).then(response => { return webhook(hook, this);
const again = wait(this.update.timeout * 1000).then(poll);
const result = response.result;
if (!result.length) {
return again;
} }
if (!this.update.offset) {
const updateId = result[result.length - 1].update_id;
this.update.offset = updateId;
}
if (this.update) {
this.update.offset += 1;
}
this.emit('update', result);
result.forEach(res => {
let text = res.message.text;
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(({pattern}) => pattern.test(text));
if (!ev) {
return;
}
ev.listener(res.message);
});
return again;
});
}.bind(this);
return this.api.getMe().then(response => { return this.api.getMe().then(response => {
this.info = response.result; this.info = response.result;
return poll();
this.on('update', this._update);
if (hook) {
return webhook(hook, this);
} else {
return poll(this);
}
}); });
} }
@ -142,10 +127,36 @@ export default class Bot extends EventEmitter {
send(message) { send(message) {
return message.send(this).catch(console.error); return message.send(this).catch(console.error);
} }
/**
* The internal update event listener, used to parse messages and fire
* command/get events - YOU SHOULD NOT USE THIS
*
* @param {object} update
*/
_update(update) {
if (!this.update.offset) {
const updateId = update[update.length - 1].update_id;
this.update.offset = updateId;
}
if (this.update) {
this.update.offset += 1;
} }
const wait = (miliseconds) => { update.forEach(res => {
return new Promise(resolve => { let text = res.message.text;
setTimeout(resolve, miliseconds); if (text.startsWith('/')) {
// Commands are sent in /command@thisusername format in groups
const regex = new RegExp(`@${this.info.username}$`);
text = text.replace(regex, '');
}
let ev = this._userEvents.find(({pattern}) => pattern.test(text));
if (!ev) {
return;
}
ev.listener(res.message);
}); });
}; }
}

19
lib/poll.js Normal file
View File

@ -0,0 +1,19 @@
export default function poll(bot) {
return bot.api.getUpdates(bot.update).then(response => {
const again = wait(bot.update.timeout * 1000, bot).then(poll);
if (!response.result.length) {
return again;
}
bot.emit('update', response.result);
return again;
});
}
const wait = (miliseconds, value) => {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, miliseconds);
});
};

24
lib/webhook.js Normal file
View File

@ -0,0 +1,24 @@
import https from 'http';
import qs from 'qs';
import {getBody} from './fetch';
const DEFAULTS = {
server: {},
port: 443
};
export default function webhook(options = {}, bot) {
options = Object.assign(DEFAULTS, options);
return bot.api.setWebhook(options.url).then(() => {
https.createServer(options.server, (req, res) => {
return getBody(req).then(data => {
bot.emit('update', qs.parse(data).result);
res.end('OK');
});
}).listen(options.port);
});
}

View File

@ -1,6 +1,6 @@
{ {
"name": "telegram-api", "name": "telegram-api",
"version": "0.2.2", "version": "0.3.2",
"description": "Control Telegram bots easily using the new Telegram API", "description": "Control Telegram bots easily using the new Telegram API",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {