From 9beb32366d4eba0461a6d7151db1eca893248f95 Mon Sep 17 00:00:00 2001 From: yago Date: Mon, 29 Jun 2015 00:37:40 +0200 Subject: [PATCH] First commit --- .gitignore | 1 + examples/crt.pem | 22 +++ examples/httpsWebHook.js | 12 ++ examples/key.pem | 27 ++++ examples/polling.js | 36 +++++ index.js | 1 + package.json | 30 ++++ src/telegram.js | 290 +++++++++++++++++++++++++++++++++++++++ test/bot.gif | Bin 0 -> 20660 bytes test/index.js | 148 ++++++++++++++++++++ 10 files changed, 567 insertions(+) create mode 100644 .gitignore create mode 100644 examples/crt.pem create mode 100644 examples/httpsWebHook.js create mode 100644 examples/key.pem create mode 100644 examples/polling.js create mode 100644 index.js create mode 100644 package.json create mode 100644 src/telegram.js create mode 100644 test/bot.gif create mode 100644 test/index.js 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 0000000000000000000000000000000000000000..1dfad4e71a1f3f90fc991ce155ea51cff82bbd48 GIT binary patch literal 20660 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 YDzEmcul_2q2CJ|RE3p>qlY{^OJLT4!n*aa+ literal 0 HcmV?d00001 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(); + }); + }); + }); + +});