diff --git a/.gitignore b/.gitignore index 123ae94..f9d6ed0 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,6 @@ build/Release # Dependency directory # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git node_modules + +# Test file I use to test my module +test.js diff --git a/README.md b/README.md index 26bd421..22eaa72 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ -# node-telegram-bots -Control Telegram bots easily +# Telegram Bots +Control Telegram bots easily. + +``` +npm install telegram-bots +``` + +# Example +```javascript +let Bot = require('telegram-bots'); + +let smartBot = new Bot({ + token: 'YOUR_TOKEN' +}); + +// update is an Update object as described in Telegram Bots API documentation +smartBot.on('Hi', update => { + const message = update.message, + id = message.chat.id; + + const question = 'How should I greet you?', + answers = ['Hi', 'Hello, Sir', 'Yo bro']; + + smartBot.askQuestion(id, question, answers) + .then(answer => { + smartBot.message(id, `Your answer: ${answer}`); + }, () => { + smartBot.message(id, 'Invalid answer'); + }); +}); + +smartBot.command('test', update => { + const message = update.message; + const id = message.chat.id; + + smartBot.message(id, 'Test command'); +}); + +smartBot.command('start', update => { + smartBot.message(update.message.chat.id, 'Hello!'); +}); +``` + +This will result in: diff --git a/index.js b/index.js new file mode 100644 index 0000000..75cdce6 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +require('babel/register'); +module.exports = require('./lib/index'); diff --git a/lib/.eslintrc b/lib/.eslintrc new file mode 100644 index 0000000..33fe593 --- /dev/null +++ b/lib/.eslintrc @@ -0,0 +1,5 @@ +{ + "rules": { + "camelcase": 0 + } +} diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..005c368 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,21 @@ +// API methods +import fetch from './fetch'; + +export default function API(token) { + this.token = token; +} + +API.prototype.request = function request(method, data) { + return fetch(this.token + '/' + method, data); +}; + +const methods = ['getMe', 'sendMessage', 'forwardMessage', 'sendPhoto', +'sendAudio', 'sendDocument', 'sendSticker', 'sendVideo', +'sendLocation', 'sendChatAction', 'getUserProfilePhotos', +'getUpdates']; + +methods.forEach(method => { + API.prototype[method] = function(data) { + return this.request(method, data); + }; +}); diff --git a/lib/fetch.js b/lib/fetch.js new file mode 100644 index 0000000..913b326 --- /dev/null +++ b/lib/fetch.js @@ -0,0 +1,37 @@ +import https from 'https'; +import qs from 'qs'; + +export default function(path, data) { + const post = qs.stringify(data); + + return new Promise((resolve, reject) => { + let res = ''; + + const req = https.request({ + hostname: 'api.telegram.org', + method: data ? 'POST' : 'GET', + path: '/bot' + path, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }, response => { + response.on('data', chunk => { + res += chunk; + }); + + response.on('end', () => { + try { + let json = JSON.parse(res); + resolve(json); + } catch(e) { + reject(e); + } + }); + }).on('error', reject); + + if (post) { + req.write(post); + } + req.end(); + }); +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..99ba524 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,154 @@ +import API from './api'; +import * as _ from './utils'; +import {EventEmitter} from 'events'; + +const DEFAULTS = { + update: { + offset: 0, + timeout: 0.5, + limit: 100 + } +}; + +export default class Bot { + constructor(options = {update: {}}) { + 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 API(this.token); + + // EventEmitter + this._events = {}; + } + + start() { + (function poll() { + this.api.getUpdates(this.update).then(response => { + setTimeout(poll.bind(this), this.update.timeout * 1000); + + const result = response.result; + if (!result.length) { + return; + } + + 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', response.result); + result.forEach(res => { + const text = res.message.text; + if (text.indexOf('/') === 0) { + this.emit('command:' + text.slice(1), res); + } else { + this.emit(text, res); + } + }); + }); + }.bind(this)()); + } + + command(cmd, listener) { + return this.on(`command:${cmd}`, listener); + } + + message(chat, text, options) { + return new Promise(resolve => { + console.log(this.keyboard); + this.api.sendMessage(Object.assign({ + chat_id: chat, + text, + reply_markup: this.keyboard + }, options)); + + if (JSON.parse(this.keyboard).one_time_keyboard) { + this.keyboard = ''; + } + + this.on('update', function listener(result) { + const update = result.find(({message}) => { + return message.chat.id === chat; + }); + + if (update) { + resolve(update); + + this.removeListener('update', listener); + } + }); + }); + } + + askQuestion(chat, title, answers = []) { + const text = title + '\n\n' + answers.reduce((a, b, i) => { + return a + `${i}. ${b}\n`; + }, ''); + + return new Promise((resolve, reject) => { + const rows = [answers]; + this.keyboard(rows, false, true).force() + .message(chat, text).then(update => { + const message = update.message; + let answer; + + if (_.isNumber(message.text)) { + answer = answers[+message.text]; + } else { + answer = answers.find(a => a === message.text); + } + + if (answer) { + resolve(answer, update); + } else { + reject(update); + } + }); + }); + } + + keyboard(rows, resize = false, oneTime = false, selective) { + this.keyboard = JSON.stringify({ + keyboard: rows, + resize_keyboard: resize, + one_time_keyboard: oneTime, + selective + }); + + return this; + } + + hideKeyboard(selective) { + this.keyboard = JSON.stringify({ + hide_keyboard: true, + selective + }); + + return this; + } + + force(enable = true, selective) { + const keyboard = JSON.parse(this.keyboard); + keyboard.force_reply = enable; + keyboard.selective = selective; + + this.keyboard = JSON.stringify(keyboard); + return this; + } + + wait(miliseconds) { + const self = this; + + return function(resolve) { + setTimeout(resolve.bind(self), miliseconds); + }; + } +} + +Bot.prototype = Object.assign(Bot.prototype, EventEmitter.prototype); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..333efa2 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,3 @@ +export function isNumber(string) { + return !isNaN(parseFloat(string)); +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bda9ef2 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "telegram-bots", + "version": "0.1.0", + "description": "Control Telegram bots easily", + "main": "index.js", + "scripts": { + "test": "grunt test" + }, + "repository": { + "type": "git", + "url": "https://github.com/mdibaiee/node-telegram-bots" + }, + "keywords": [ + "Telegram", + "bot", + "node", + "module" + ], + "author": "Mahdi Dibaiee", + "license": "MIT", + "bugs": { + "url": "https://github.com/mdibaiee/node-telegram-bots/issues" + }, + "homepage": "https://github.com/mdibaiee/node-telegram-bots", + "dependencies": { + "babel": "^5.6.8", + "qs": "^3.1.0" + } +}