commit 9beb32366d4eba0461a6d7151db1eca893248f95 Author: yago Date: Mon Jun 29 00:37:40 2015 +0200 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/crt.pem b/examples/crt.pem new file mode 100644 index 0000000..1a65763 --- /dev/null +++ b/examples/crt.pem @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDmTCCAoGgAwIBAgIJAPz/mOxHHCRKMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNV +BAYTAlVTMQ0wCwYDVQQIDARVdGFoMQ4wDAYDVQQHDAVQcm92bzEjMCEGA1UECgwa +QUNNRSBTaWduaW5nIEF1dGhvcml0eSBJbmMxEDAOBgNVBAMMB3lhZ28ubWUwHhcN +MTUwNjI4MTk0MjAxWhcNMTgwNzA2MTk0MjAxWjBjMQswCQYDVQQGEwJVUzENMAsG +A1UECAwEVXRhaDEOMAwGA1UEBwwFUHJvdm8xIzAhBgNVBAoMGkFDTUUgU2lnbmlu +ZyBBdXRob3JpdHkgSW5jMRAwDgYDVQQDDAd5YWdvLm1lMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAw9YiYXN1s5KcoZy7UZyiXULpTrYPhlPhzlyJJdwg +e61C/swbqtnh/+fPZp8g8a15ond9ShUvWLcxeoDBzxn0hJIEe+DlNNHUAdWoTWUx +OP4hHDA6wCFepHWBlw10AoKAjoQA+nCX6NrdiFTpbodkEK0H4uOSCt37H616kdKU +wRgXlca2Kw88UQ0qhKteb5hYD5tm4aCv6eRCqwYdYKUG+D1uJuJ+YZmaaIXp/5QZ +q3a6mFsKLtUC33bhZZPr1qjh3zwF2JTZX1WFAxUHNxY5NVchUYDHjw0djXvw85il +iwWKFjFXfvk8WTfW3Ge3754BhYSt92Qj6BROD2AODhI8jwIDAQABo1AwTjAdBgNV +HQ4EFgQUpgp5hovXcW+eIb3xRkF1KSJb/rwwHwYDVR0jBBgwFoAUpgp5hovXcW+e +Ib3xRkF1KSJb/rwwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAjnKW ++3lHe92Ut9XJdqGJsuRV5OUh8suOicz+AXtqUdoG9xbkv5N6Ynt+r06NjnYgTIzk +i+9fXBZLrXH8qNT2PTzErs0LMXPWxWbiZwY9mI2z/xW/K6CFjb1h33hk+ypwTr1J +Q1Eqy77FXKfQ2Y8kNLARSkvUEMm7UnVbUqRbA8AlWk9HZmoPHYfKPRGRVeIugH76 +b6Gm3ztmIgTZQ88+DxfedIjPib3LPsHIXrA2Qd8yrIaYDiE2HMMJ5q3SYdRY4yYB +2a3P7jCPZfKVKpRE0J0yeNH+wQL0bzCMbl2wBUhivXD+sM00Xe3a22eAYbNgLdEg +4Hvd/YIKm9yOjRolmw== +-----END CERTIFICATE----- diff --git a/examples/httpsWebHook.js b/examples/httpsWebHook.js new file mode 100644 index 0000000..b698197 --- /dev/null +++ b/examples/httpsWebHook.js @@ -0,0 +1,12 @@ +var TelegramBot = require('../src/telegram'); + +var options = { + webHook: { + port: 443, + key: __dirname+'/key.pem', + cert: __dirname+'/crt.pem' + } +}; + +var bot = new TelegramBot('BOTTOKEN', options); +bot.setWebHook('IP:PORT'); diff --git a/examples/key.pem b/examples/key.pem new file mode 100644 index 0000000..427ad65 --- /dev/null +++ b/examples/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAw9YiYXN1s5KcoZy7UZyiXULpTrYPhlPhzlyJJdwge61C/swb +qtnh/+fPZp8g8a15ond9ShUvWLcxeoDBzxn0hJIEe+DlNNHUAdWoTWUxOP4hHDA6 +wCFepHWBlw10AoKAjoQA+nCX6NrdiFTpbodkEK0H4uOSCt37H616kdKUwRgXlca2 +Kw88UQ0qhKteb5hYD5tm4aCv6eRCqwYdYKUG+D1uJuJ+YZmaaIXp/5QZq3a6mFsK +LtUC33bhZZPr1qjh3zwF2JTZX1WFAxUHNxY5NVchUYDHjw0djXvw85iliwWKFjFX +fvk8WTfW3Ge3754BhYSt92Qj6BROD2AODhI8jwIDAQABAoIBAQCga6gMNh2DtSTT +imUzrGCgjvA5RxAelFYTyl+agOCnDz4jJKXBZewoygZuZQoCj31lJgafCg2X2bER +Tan1caiIdGhx5b88bmoB+rh8ddlFe3857RQjUPKLO6qlRyLx719J3z5B6Lu3xpnU +VOJHZWcF9gfQx2RZvI862svd6idqqFfKRVr7jxur1VuTQpk6g0xi6GnFk9s6sPw9 +ChT6ykxzx+fQmYzeEW6SbWilOnm9BGuAEI2G7/mDQ6NFGFvFdPivI908vHPGbhFz +Ifdwt5F9NwQrSzYaDnYzCWrEmqSz9uRmX5DD9FwFokjN2d6o3V0/+1BrWLKDBDj+ +jYcOV5IBAoGBAOFLTbfRLPP0z4vgTX8FeaZXreJQPblHmJQNwvJEYiiug4aDXCLv +uBEVR+H1Y/Pm1U4s5LNESg2pOC0IZOElvAck+SY/K1KZFI9EzBLa0aybFOuLaHY1 +Z9cZfc1Cg3Vmpqnkyqahi+Tt1U/nayL3DFNcIwI30DweS+KLUmjIxxMBAoGBAN6H +ByD0+d3pfqkusTI3GT1NfWMBwS+usCayLP1gMpA+0tT+/lnYLLbmINaD6hoztWWD +OUZ7PM3HkOXKly2bfKxlT8Bi3b0QpNyd54ybj4/60JLRAO5OU773an8MMsov+q1V +xWYGVMnNihXFFVGaIK8dG/2mYomHjbzx3az/EZ+PAoGAJs3pnPeSXpKUDOudbXtr +8JK5iHl5qCgEx7t3EHNm1Mr6LHkDraDMe2TG9MxnYuMnakehPJ9OgfvbiSYg+gad +1D0yDLxkod1sBSE8ZSL7aldrywY//9xC/nGNkYUbT2VW33xgy0KX7d5pF1IsyeDz +ZohAH2mtnC07tNF6aEHsyAECgYAJ3EHcm/5WbvpF1OPVLcvYg46CzJka28rCbDLC +J3kWGzKMbaAnqwSQNjJOTxoYfyISlXX8QYm4NJefFxML2k/z86lNBRR+RDaJ8BVK +jboWzy5e0xQPezkKxTva1VeKzgV1mM9ebflj18++lzUSoJnCKLAM1UqYfYEyViVU +fRjy0QKBgQCopUy7KDdKngBrSQwI9lMi1/bZJTXw1WktLZRma3uIw8uBKB2Fyf4/ +7xFo49Ha7l1W38PfkqOS+539V8cJSyyJKq+PgBQ8fuLCplCDeZCieSiYm+FkpIr0 +4V+hEMIkVuUBDwCbyMM5mUH+sBVtmzNYDRgYa5QN4FIBk8VcBgsIhw== +-----END RSA PRIVATE KEY----- diff --git a/examples/polling.js b/examples/polling.js new file mode 100644 index 0000000..6b40ad5 --- /dev/null +++ b/examples/polling.js @@ -0,0 +1,36 @@ +var TelegramBot = require('../src/telegram'); +var request = require('request'); + +var options = { + polling: true +}; + +var token = 'YOUR_TELEGRAM_BOT_TOKEN'; + +var bot = new TelegramBot(token, options); +bot.getMe().then(function (me) { + console.log('Hi my name is %s!', me.username); +}); +bot.on('message', function (msg) { + var chatId = msg.chat.id; + if (msg.text == '/photo') { + // From file + var photo = __dirname+'/../test/bot.gif'; + bot.sendPhoto(chatId, photo, {caption: "I'm a bot!"}); + } + if (msg.text == '/audio') { + var url = 'https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg'; + // From HTTP request! + var audio = request(url); + bot.sendAudio(chatId, audio) + .then(function (resp) { + // Forward the msg + var messageId = resp.message_id; + bot.forwardMessage(chatId, chatId, messageId); + }); + } + if (msg.text == '/help') { + var opts = {reply_to_message_id: msg.message_id}; + bot.sendMessage(chatId, 'This is only a test :D', opts); + } +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..adb731f --- /dev/null +++ b/index.js @@ -0,0 +1 @@ +module.exports = require('./src/telegram'); diff --git a/package.json b/package.json new file mode 100644 index 0000000..f127be3 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "telegram-bot-api", + "version": "0.1.0", + "description": "Telegram Bot API", + "main": "index.js", + "directories": { + "test": "test", + "src": "src" + }, + "keywords": [ + "telegram", + "telegram bot", + "telegram bot api", + "bot" + ], + "scripts": { + "test": "./node_modules/.bin/mocha test/index.js" + }, + "author": "Yago Pérez ", + "license": "MIT", + "dependencies": { + "bluebird": "^2.9.30", + "mime": "^1.3.4", + "request": "^2.58.0" + }, + "devDependencies": { + "mocha": "^2.2.5", + "should": "^7.0.1" + } +} diff --git a/src/telegram.js b/src/telegram.js new file mode 100644 index 0000000..024c89f --- /dev/null +++ b/src/telegram.js @@ -0,0 +1,290 @@ +var EventEmitter = require('events').EventEmitter; +var Promise = require("bluebird"); +var request = require("request"); +var stream = require('stream'); +var https = require('https'); +var http = require('http'); +var util = require('util'); +var mime = require('mime'); +var path = require('path'); +var URL = require('url'); +var fs = require('fs'); + +var requestPromise = Promise.promisify(request); + +/** + * Telegram Bot API. Suppor for WebHooks and long polling. Emits `message` when + * a message arrives. + * + * @class TelegramBot + * @constructor + * @param {String} token Bot Token + * @param {Object} [options] + * @param {Boolean|Object} [options.polling=false] Set true to enable polling + * @param {String|Number} [options.polling.timeout=4] Polling time + * @param {Boolean|Object} [options.webHook=false] Set true to enable WebHook + * @param {String} [options.webHook.key] PEM private key to webHook server + * @param {String} [options.webHook.cert] PEM certificate key to webHook server + * @see https://core.telegram.org/bots/api + */ +var TelegramBot = function (token, options) { + options = options || {}; + this.token = token; + this.offset = 0; + this._webServer = null; + + if (options.polling) { + // By default polling for 4 seconds + var timeout = options.polling.timeout || 4; + this._polling(timeout); + } + + if (options.webHook) { + var port = options.webHook.port || 8443; + var key = options.webHook.key; + var cert = options.webHook.cert; + this._configureWebHook(port, key, cert); + } +}; + +util.inherits(TelegramBot, EventEmitter); + +TelegramBot.prototype._configureWebHook = function (port, key, cert) { + var protocol = 'HTTP'; + var self = this; + + if (key && cert) { // HTTPS Server + protocol = 'HTTPS'; + var options = { + key: fs.readFileSync(key), + cert: fs.readFileSync(cert) + }; + this._webServer = https.createServer(options, function (req, res) { + self._requestListener.call(self, req, res); + }); + } else { // HTTP Server + this._webServer = http.createServer(function (req, res) { + self._requestListener.call(self, req, res); + }); + } + + this._webServer.listen(port, function () { + console.log(protocol+" WebHook listening on:", port); + }); +}; + +TelegramBot.prototype._requestListener = function (req, res) { + var self = this; + if (req.url == '/bot' && req.method == 'POST') { + var fullBody = ''; + req.on('data', function (chunk) { + fullBody += chunk.toString(); + }); + req.on('end', function () { + try { + var data = JSON.parse(fullBody); + self.emit('message', data); + } catch (error) { + console.error(error); + } + res.end('OK\n'); + }); + } else { + res.end('OK\n'); + } +}; + +TelegramBot.prototype._processUpdates = function (updates) { + for (var i = 0; i < updates.length; i++) { + if (updates[i].message) { + this.emit('message', updates[i].message); + } + } +}; + +TelegramBot.prototype._polling = function (timeout) { + var self = this; + this.getUpdates(timeout).then(function (data) { + self._processUpdates(data); + self._polling(timeout); + }); +}; + +TelegramBot.prototype._request = function (path, options) { + if (!this.token) { + throw new Error('Telegram Bot Token not provided!'); + } + options = options || {}; + options.url = URL.format({ + protocol: 'https', + host: 'api.telegram.org', + pathname: '/bot'+this.token+'/'+path + }); + + return requestPromise(options) + .then(function (resp) { + if (resp[0].statusCode != 200) { + throw new Error(resp[0].statusCode+' '+resp[0].body); + } + var data = JSON.parse(resp[0].body); + if (data.ok) { + return data.result; + } else { + throw new Error(data.error_code+' '+data.description); + } + }); +}; + +/** + * Returns basic information about the bot in form of a `User` object. + * @return {Promise} + */ +TelegramBot.prototype.getMe = function () { + var path = 'getMe'; + return this._request(path); +}; + +/** + * Specify a url to receive incoming updates via an outgoing webHook. + * @param {String} url URL + */ +TelegramBot.prototype.setWebHook = function (url) { + var path = 'setWebHook'; + var qs = {url: url}; + return this._request(path, {method: 'POST'}) + .then(function (resp) { + if (!resp) { + throw new Error(resp); + } + }); +}; + +/** + * Use this method to receive incoming updates using long polling + * @param {Number|String} [timeout] Timeout in seconds for long polling. + * @param {Number|String} [limit] Limits the number of updates to be retrieved. + * @param {Number|String} [offset] Identifier of the first update to be returned. + * @return {Promise} Updates + * @see https://core.telegram.org/bots/api#getupdates + */ +TelegramBot.prototype.getUpdates = function (timeout, limit, offset) { + var self = this; + var query = { + offset: offset || this.offset+1, + limit: limit, + timeout: timeout + }; + + return this._request('getUpdates', {qs: query}) + .then(function (result) { + var last = result[result.length-1]; + if (last) { + self.offset = last.update_id; + } + return result; + }); +}; + +/** + * Send text message. + * @param {Number|String} chatId Unique identifier for the message recipient + * @param {Sting} text Text of the message to be sent + * @param {Object} [options] Additional Telegram query options + * @return {Promise} + * @see https://core.telegram.org/bots/api#sendmessage + */ +TelegramBot.prototype.sendMessage = function (chatId, text, options) { + var query = options || {}; + query.chat_id = chatId; + query.text = text; + return this._request('sendMessage', {qs: query}); +}; + +/** + * Forward messages of any kind. + * @param {Number|String} chatId Unique identifier for the message recipient + * @param {Number|String} fromChatId Unique identifier for the chat where the + * original message was sent + * @param {Number|String} messageId Unique message identifier + * @return {Promise} + */ +TelegramBot.prototype.forwardMessage = function (chatId, fromChatId, messageId) { + var query = { + chat_id: chatId, + from_chat_id: fromChatId, + message_id: messageId + }; + return this._request('forwardMessage', {qs: query}); +}; + +TelegramBot.prototype._formatSendData = function (type, data) { + var formData; + var fileName; + var fileId; + if (data instanceof stream.Stream) { + fileName = path.basename(data.path); + formData = {}; + formData[type] = { + value: data, + options: { + filename: fileName, + contentType: mime.lookup(fileName) + } + }; + } else if (fs.existsSync(data)) { + fileName = path.basename(data); + formData = {}; + formData[type] = { + value: fs.createReadStream(data), + options: { + filename: fileName, + contentType: mime.lookup(fileName) + } + }; + } else { + fileId = data; + } + return [formData, fileId]; +}; + +/** + * Send photo + * @param {Number|String} chatId Unique identifier for the message recipient + * @param {String|stream.Stream} photo A file path or a Stream. Can + * also be a `file_id` previously uploaded + * @param {Object} [options] Additional Telegram query options + * @return {Promise} + * @see https://core.telegram.org/bots/api#sendphoto + */ +TelegramBot.prototype.sendPhoto = function (chatId, photo, options) { + var opts = { + qs: options || {} + }; + opts.qs.chat_id = chatId; + var content = this._formatSendData('photo', photo); + opts.formData = content[0]; + opts.qs.photo = content[1]; + return this._request('sendPhoto', opts); +}; + +/** + * Send audio + * @param {Number|String} chatId Unique identifier for the message recipient + * @param {String|stream.Stream} audio A file path or a Stream. Can + * also be a `file_id` previously uploaded. + * @param {Object} [options] Additional Telegram query options + * @return {Promise} + * @see https://core.telegram.org/bots/api#sendaudio + */ +TelegramBot.prototype.sendAudio = function (chatId, audio, options) { + var opts = { + qs: options || {} + }; + opts.qs.chat_id = chatId; + var content = this._formatSendData('audio', audio); + opts.formData = content[0]; + opts.qs.audio = content[1]; + return this._request('sendAudio', opts); +}; + +module.exports = TelegramBot; diff --git a/test/bot.gif b/test/bot.gif new file mode 100644 index 0000000..1dfad4e Binary files /dev/null and b/test/bot.gif differ diff --git a/test/index.js b/test/index.js new file mode 100644 index 0000000..76a4655 --- /dev/null +++ b/test/index.js @@ -0,0 +1,148 @@ +var Telegram = require('../src/telegram'); +var request = require('request'); +var should = require('should'); +var fs = require('fs'); + +var TOKEN = process.env.TEST_TELEGRAM_TOKEN; +if (!TOKEN) { + throw new Error('Bot token not provided'); +} +// Telegram service if not User Id +var USERID = process.env.TEST_USER_ID || 777000; + +describe('Telegram', function () { + + describe('#emit', function () { + it('should emit a `message` on polling', function (done) { + var bot = new Telegram(TOKEN); + bot.on('message', function (msg) { + msg.should.be.an.instanceOf(Object); + bot._polling = function () {}; + done(); + }); + bot.getUpdates = function() { + return { + then: function (cb) { + cb([{update_id: 0, message: {}}]); + } + }; + }; + bot._polling(); + }); + + it('should emit a `message` on WebHook', function (done) { + var bot = new Telegram(TOKEN, {webHook: true}); + bot.on('message', function (msg) { + msg.should.be.an.instanceOf(Object); + done(); + }); + var url = 'http://localhost:8443/bot'; + request({ + url: url, + method: 'POST', + json: true, + headers: { + "content-type": "application/json", + }, + body: JSON.stringify({update_id: 0, message: {text: 'test'}}) + }); + }); + }); + + describe('#getMe', function () { + it('should return an User object', function (done) { + var bot = new Telegram(TOKEN); + bot.getMe().then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + }); + + describe('#getUpdates', function () { + it('should return an Array', function (done) { + var bot = new Telegram(TOKEN); + bot.getUpdates().then(function (resp) { + resp.should.be.an.instanceOf(Array); + done(); + }); + }); + }); + + describe('#sendMessage', function () { + it('should send a message', function (done) { + var bot = new Telegram(TOKEN); + bot.sendMessage(USERID, 'test').then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + }); + + describe('#forwardMessage', function () { + it('should forward a message', function (done) { + var bot = new Telegram(TOKEN); + bot.sendMessage(USERID, 'test').then(function (resp) { + var messageId = resp.message_id; + bot.forwardMessage(USERID, USERID, messageId) + .then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + }); + }); + + describe('#sendPhoto', function () { + var photoId; + it('should send a photo from file', function (done) { + var bot = new Telegram(TOKEN); + var photo = __dirname+'/bot.gif'; + bot.sendPhoto(USERID, photo).then(function (resp) { + resp.should.be.an.instanceOf(Object); + photoId = resp.photo[0].file_id; + done(); + }); + }); + + it('should send a photo from id', function (done) { + var bot = new Telegram(TOKEN); + // Send the same photo as before + var photo = photoId; + bot.sendPhoto(USERID, photo).then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + + it('should send a photo from fs.readStream', function (done) { + var bot = new Telegram(TOKEN); + var photo = fs.createReadStream(__dirname+'/bot.gif'); + bot.sendPhoto(USERID, photo).then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + + it('should send a photo from request Stream', function (done) { + var bot = new Telegram(TOKEN); + var photo = request('https://telegram.org/img/t_logo.png'); + bot.sendPhoto(USERID, photo).then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + }); + + describe('#sendAudio', function () { + it('should send an OGG audio', function (done) { + var bot = new Telegram(TOKEN); + var audio = request('https://upload.wikimedia.org/wikipedia/commons/c/c8/Example.ogg'); + bot.sendAudio(USERID, audio).then(function (resp) { + resp.should.be.an.instanceOf(Object); + done(); + }); + }); + }); + +});