├── .eslintignore ├── scripts ├── image.png ├── sendPhoto.example.js └── example.js ├── .gitignore ├── .editorconfig ├── .travis.yml ├── .eslintrc.js ├── bin ├── hubot.cmd └── hubot ├── test ├── hubot.stub.js └── telegram.test.js ├── LICENSE ├── src ├── groupsManager.js └── telegram.js ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules -------------------------------------------------------------------------------- /scripts/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lbb00/hubot-telegram-better/HEAD/scripts/image.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node_module 2 | node_modules 3 | 4 | # npm 5 | package-lock.json 6 | 7 | # Logs 8 | logs 9 | *.log 10 | 11 | # Groups data 12 | groups.data -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "9" 5 | install: 6 | - npm install 7 | script: 8 | - npm run test 9 | branches: 10 | only: 11 | - master 12 | - /^greenkeeper/.*$/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'standard', 4 | rules:{ 5 | camelcase: 0, 6 | nodeprecatedapi: 0 7 | }, 8 | globals: { 9 | describe: false, 10 | it: false 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /bin/hubot.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | SETLOCAL 4 | SET PATH = node_modules\.bin;node_modules\hubot\node_modules\.bin;%PATH% 5 | 6 | ::config 7 | SET TELEGRAM_TOKEN= 8 | SET TELEGRAM_WEBHOOK= 9 | SET TELEGRAM_INTERVAL=500 10 | 11 | node_modules/.bin/hubot -a telegram-better %* 12 | -------------------------------------------------------------------------------- /bin/hubot: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH" 6 | 7 | # config 8 | export PORT=8091 9 | export TELEGRAM_TOKEN= 10 | export TELEGRAM_WEBHOOK= 11 | export TELEGRAM_INTERVAL=500 12 | 13 | exec node_modules/.bin/hubot -a telegram-better "$@" 14 | -------------------------------------------------------------------------------- /test/hubot.stub.js: -------------------------------------------------------------------------------- 1 | // Create a basic Hubot stub object so 2 | // we don't have to include the Hubot dependency 3 | // in our tests 4 | 5 | var noop = function () {} 6 | 7 | module.exports = { 8 | name: 'TestBot', 9 | alias: 'TestAliasBot', 10 | logger: { 11 | info: noop, 12 | warning: noop, 13 | error: noop, 14 | debug: noop 15 | }, 16 | brain: {} 17 | } 18 | -------------------------------------------------------------------------------- /scripts/sendPhoto.example.js: -------------------------------------------------------------------------------- 1 | // Description: 2 | // This script uses custom Telegram functionality to deliver a photo 3 | // to a user using the Telegram sendPhoto call 4 | 5 | const fs = require('fs') 6 | const path = require('path') 7 | 8 | module.exports = robot => { 9 | robot.hear(/send photo/i, res => { 10 | robot.emit('telegram:invoke', 'sendPhoto', { 11 | chat_id: res.message.room, 12 | photo: fs.createReadStream(path.resolve(__dirname, './image.png')) 13 | }, (error, response) => { 14 | console.log(error) 15 | console.log(response) 16 | }) 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /scripts/example.js: -------------------------------------------------------------------------------- 1 | module.exports = robot => { 2 | robot.respond(/bot/i, res => { 3 | res.send('Is me') 4 | }) 5 | 6 | robot.hear(/bot/i, res => { 7 | res.send('I am here') 8 | }) 9 | 10 | /** 11 | * Push message 12 | */ 13 | robot.router.post('/push', (req, res) => { 14 | // Adapter.push(message, config) 15 | // 16 | // message - string 17 | // config - [{ rule, type = 'all'|'group'|'friend', reg = true }={}] 18 | robot.adapter.push(`${new Date()} This is a push.`, { 19 | rule: name => { 20 | return name.match('bot') 21 | } 22 | }).then(msg => { 23 | res.send({ code: 0, err_msg: '' }) 24 | }).catch(err => { 25 | res.send({ code: -1, err_msg: `Error: ${err}` }) 26 | }) 27 | }) 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 onelong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/groupsManager.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | 4 | class GroupsManager { 5 | constructor () { 6 | this.saveLock = '' 7 | try { 8 | this.groups = JSON.parse(fs.readFileSync(path.resolve(process.cwd(), './groups.data')).toString()) 9 | } catch (e) { 10 | this.groups = [] 11 | } 12 | } 13 | 14 | _save () { 15 | if (this.saveLock) return 16 | this.saveLock = setTimeout(() => { 17 | fs.writeFile(path.resolve(process.cwd(), './groups.data'), JSON.stringify(this.groups), err => { 18 | // Unlock 19 | this.saveLock = '' 20 | if (err) console.log(err) 21 | }) 22 | }, 0) 23 | } 24 | 25 | _set (group) { 26 | let len = this.groups.length 27 | while (len--) { 28 | if (this.groups[len].id === group.id) { 29 | this.groups[len] = group 30 | return 31 | } 32 | } 33 | this.groups.push(group) 34 | } 35 | 36 | update (id, opts) { 37 | let group = { 38 | id, 39 | name: opts.name 40 | } 41 | this._set(group) 42 | this._save() 43 | } 44 | 45 | delete (id) { 46 | let len = this.groups.length 47 | while (len--) { 48 | if (this.groups[len].id === id) { 49 | delete this.groups[len] 50 | return 51 | } 52 | } 53 | } 54 | } 55 | 56 | module.exports = new GroupsManager() 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-telegram-better", 3 | "version": "0.0.7", 4 | "description": "A better hubot telegram adapter", 5 | "main": "./src/telegram.js", 6 | "scripts": { 7 | "start": "bin/hubot", 8 | "eslint": "eslint --fix ./", 9 | "test": "mocha", 10 | "link": "npm link ../hubot-telegram-better" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/loveonelong/hubot-telegram-better.git" 15 | }, 16 | "keywords": [ 17 | "hubot", 18 | "adapter", 19 | "telegram", 20 | "telegram hubot", 21 | "hubot telegram", 22 | "better", 23 | "hubot telegram better" 24 | ], 25 | "author": "one long ", 26 | "contributors": [ 27 | "onelong (http://www.onelong.org)" 28 | ], 29 | "license": "MIT", 30 | "bugs": { 31 | "url": "https://github.com/loveonelong/hubot-telegram-better/issues" 32 | }, 33 | "homepage": "https://github.com/loveonelong/hubot-telegram-better", 34 | "dependencies": { 35 | "hubot": "^3.1.1", 36 | "socks5-https-client": "^1.2.1", 37 | "telegrambot": "^0.1.1" 38 | }, 39 | "devDependencies": { 40 | "chai": "^4.1.2", 41 | "eslint": "^6.0.0", 42 | "eslint-config-standard": "^14.0.0", 43 | "eslint-plugin-import": "^2.14.0", 44 | "eslint-plugin-node": "^11.1.0", 45 | "eslint-plugin-promise": "^4.0.0", 46 | "eslint-plugin-standard": "^4.0.0", 47 | "mocha": "^5.2.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hubot-telegram-better 2 | 3 | [![Build Status](https://travis-ci.org/loveonelong/hubot-telegram-better.svg?branch=master)](https://travis-ci.org/loveonelong/hubot-telegram-better) 4 | [![Greenkeeper badge](https://badges.greenkeeper.io/loveonelong/hubot-telegram-better.svg)](https://greenkeeper.io/) 5 | [![Npm](https://img.shields.io/npm/v/hubot-telegram-better.svg)](https://www.npmjs.com/package/hubot-telegram-better) 6 | 7 | A better [hubot](https://hubot.github.com/docs/) adapter for interfacting with the [Telegram Bot API](https://core.telegram.org/bots/api) 8 | 9 | > I don't have enough energy to maintain this project. If you have some solutions, welcome pr. 10 | 11 | ## Installation & Usage 12 | 13 | First of reading the docs on how to create a new [Telegram Bot](https://core.telegram.org/bots#botfather). Once you have a bot created, follow these steps: 14 | 15 | * `npm install --save hubot-telegram-better` 16 | * Set the environment variables specified in **Configuration** 17 | * Run hubot `bin/hubot -a telegram` 18 | 19 | ## Configuration 20 | 21 | This adapter uses the following environment variables: 22 | 23 | **TELEGRAM_TOKEN** (required) 24 | 25 | The token that the [BotFather](https://core.telegram.org/bots#botfather) gives you 26 | 27 | **TELEGRAM_WEBHOOK** (optional) 28 | 29 | You can specify a [webhook](https://core.telegram.org/bots/api#setwebhook) URL. The adapter will register TELEGRAM_WEBHOOK/TELEGRAM_TOKEN with Telegram and listen there. 30 | 31 | **TELEGRAM_INTERVAL** (optional) 32 | 33 | You can specify the interval (in milliseconds) in which the adapter will poll Telegram for updates. This option only applies if you are not using a [webhook](https://core.telegram.org/bots/api#setwebhook). 34 | 35 | **TELEGRAM_AUTO_MARKDOWN** (optional) 36 | 37 | You may `disable` auto Mardown feature if desired. It is `enabled` by default, but detection is very simple and may break with some external scripts (like hubot-help). 38 | 39 | ## Telegram Specific Functionality (ie. Stickers, Images) 40 | 41 | If you want to create a script that relies on specific Telegram functionality that is not available to Hubot normally, you can do so by emitting the `telegram:invoke` event in your script: 42 | 43 | ``` javascript 44 | 45 | module.exports = function (robot) { 46 | 47 | robot.hear(/send sticker/i, function (res) { 48 | 49 | # https://core.telegram.org/bots/api#sendsticker 50 | 51 | robot.emit('telegram:invoke', 'sendSticker', { chat_id: xxx, sticker: 'sticker_id' }, function (error, response) { 52 | console.log(error) 53 | console.log(response) 54 | }); 55 | }); 56 | }; 57 | 58 | ``` 59 | 60 | **Note:** An example script of how to use this is located in the `scripts/` folder 61 | 62 | If you want to supplement your message delivery with extra features such as **markdown** syntax or **keyboard** replies, you can specify these settings on the `res.envelope` variable in your plugin. 63 | 64 | ```javascript 65 | 66 | robot.respond(/(.*)/i, function (res) { 67 | res.envelope.telegram = { reply_markup: { keyboard: [["test"]] }} 68 | 69 | res.reply("Select the option from the keyboard specified.") 70 | } 71 | 72 | ``` 73 | 74 | **Note:** Markdown will automatically be parsed if the supported markdown characters are included. You can override this by specifying the `parse_mode` value in the `envelope.telegram` key. 75 | 76 | ## Development 77 | 78 | First, you need clone this repo to your local. 79 | 80 | **install package** 81 | 82 | ```bash 83 | npm install 84 | ``` 85 | 86 | **link** 87 | 88 | ```bash 89 | npm run link 90 | ``` 91 | 92 | **run** 93 | 94 | ```bash 95 | npm start 96 | ``` 97 | 98 | or 99 | 100 | ```bash 101 | bin/hubot 102 | ``` 103 | 104 | ## Test 105 | 106 | ```bash 107 | npm run test 108 | ``` 109 | -------------------------------------------------------------------------------- /test/telegram.test.js: -------------------------------------------------------------------------------- 1 | const hubot = require('./hubot.stub') 2 | const expect = require('chai').expect 3 | 4 | let telegram = require('./../src/telegram').use(hubot) 5 | describe('Telegram', function () { 6 | describe('#applyExtraOptions()', function () { 7 | it('should automatically add the markdown option if the text contains markdown characters', function () { 8 | let message = { text: 'normal' } 9 | 10 | message = telegram.applyExtraOptions(message) 11 | expect(message.parse_mode).to.a('undefined') 12 | 13 | message = { text: 'markdown *message*' } 14 | message = telegram.applyExtraOptions(message) 15 | expect(message.parse_mode).to.equal('Markdown') 16 | 17 | message = { text: 'markdown _message_' } 18 | message = telegram.applyExtraOptions(message) 19 | expect(message.parse_mode).to.equal('Markdown') 20 | 21 | message = { text: 'markdown `message`' } 22 | message = telegram.applyExtraOptions(message) 23 | expect(message.parse_mode).to.equal('Markdown') 24 | 25 | message = { text: 'markdown [message](http://link.com)' } 26 | message = telegram.applyExtraOptions(message) 27 | expect(message.parse_mode).to.equal('Markdown') 28 | }) 29 | 30 | it('should apply any extra options passed the message envelope', function () { 31 | let message = { text: 'test' } 32 | let extra = { extra: true, nested: { extra: true }, nullObject: null } 33 | message = telegram.applyExtraOptions(message, extra) 34 | 35 | expect(extra.extra).to.equal(message.extra) 36 | expect(extra.nested.extra).to.equal(message.nested.extra) 37 | expect(extra.nullObject).to.equal(message.nullObject) 38 | 39 | // Mock the API object 40 | telegram.api = { 41 | invoke: function (method, opts, cb) { 42 | expect(extra.extra).to.equal(opts.extra) 43 | expect(extra.nested.extra).to.equal(opts.nested.extra) 44 | cb.apply(this, [null, {}]) 45 | } 46 | } 47 | 48 | telegram.send({ telegram: extra }, 'text') 49 | }) 50 | }) 51 | 52 | describe('#createUser()', function () { 53 | it('should use the new user object if the first_name or last_name changed', function () { 54 | telegram.robot.brain.data = { users: [] } 55 | 56 | let original = { 57 | id: 1234, 58 | first_name: 'Firstname', 59 | last_name: 'Surname', 60 | username: 'username' 61 | } 62 | 63 | telegram.robot.brain.userForId = function () { 64 | return original 65 | } 66 | 67 | let user = { 68 | id: 1234, 69 | first_name: 'Updated', 70 | last_name: 'Surname', 71 | username: 'username' 72 | } 73 | let result 74 | result = telegram.createUser(original, 1) 75 | expect(original.first_name).to.equal(result.first_name) 76 | 77 | result = telegram.createUser(user, 1) 78 | expect(user.first_name).to.equal(result.first_name) 79 | }) 80 | 81 | it('should use the new user object if the username changed', function () { 82 | telegram.robot.brain.data = { users: [] } 83 | 84 | let original = { 85 | id: 1234, 86 | first_name: 'Firstname', 87 | last_name: 'Surname', 88 | username: 'old' 89 | } 90 | 91 | telegram.robot.brain.userForId = function () { 92 | return original 93 | } 94 | 95 | let user = { 96 | id: 1234, 97 | first_name: 'Firstname', 98 | last_name: 'Surname', 99 | username: 'username' 100 | } 101 | 102 | let result = telegram.createUser(user, 1) 103 | expect(user.username).to.equal(result.username) 104 | }) 105 | }) 106 | 107 | describe('#send()', function () { 108 | it('should not split messages below or equal to 4096 characters', function () { 109 | let called = 0 110 | 111 | let message = '' 112 | for (let i = 0; i < 4096; i++) message += 'a' 113 | 114 | // Mock the API object 115 | telegram.api = { 116 | invoke: function (method, opts, cb) { 117 | expect(opts.text.length).to.equal(4096) 118 | called++ 119 | cb.apply(this, [null, {}]) 120 | } 121 | } 122 | 123 | telegram.send({ room: 1 }, message) 124 | expect(called).to.equal(1) 125 | }) 126 | 127 | it('should split messages when they are above 4096 characters', function () { 128 | let called = 0 129 | 130 | let message = '' 131 | for (let i = 0; i < 5000; i++) message += 'a' 132 | 133 | // Mock the API object 134 | telegram.api = { 135 | invoke: function (method, opts, cb) { 136 | let offset = called * 4096 137 | expect(opts.text.length).to.equal(message.substring(offset, offset + 4096).length) 138 | called++ 139 | cb.apply(this, [null, {}]) 140 | } 141 | } 142 | 143 | telegram.send({ room: 1 }, message) 144 | expect(called).to.equal(2) 145 | }) 146 | 147 | it('should not split messages on new line characters', function () { 148 | let called = 0 149 | 150 | let message = '' 151 | for (let i = 0; i < 1000; i++) message += 'a' 152 | message += '\n' 153 | for (let i = 0; i < 1000; i++) message += 'b' 154 | message += '\n' 155 | for (let i = 0; i < 1000; i++) message += 'c' 156 | message += '\n' 157 | for (let i = 0; i < 1000; i++) message += 'd' 158 | message += '\n' 159 | for (let i = 0; i < 1000; i++) message += 'e' 160 | message += '\n' 161 | 162 | // Mock the API object 163 | telegram.api = { 164 | invoke: function (method, opts, cb) { 165 | let offset = called * 4096 166 | expect(opts.text.length).to.equal(message.substring(offset, offset + 4096).length) 167 | called++ 168 | cb.apply(this, [null, {}]) 169 | } 170 | } 171 | 172 | telegram.send({ room: 1 }, message) 173 | expect(called).to.equal(2) 174 | }) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /src/telegram.js: -------------------------------------------------------------------------------- 1 | const { Adapter, TextMessage, EnterMessage, LeaveMessage, TopicMessage, CatchAllMessage } = require('hubot/es2015') 2 | const Telegrambot = require('telegrambot') 3 | const groupsManager = require('./groupsManager.js') 4 | 5 | class TelegrambotAdapter extends Adapter { 6 | constructor () { 7 | super(...arguments) 8 | this.token = process.env['TELEGRAM_TOKEN'] || '' 9 | this.webhook = process.env['TELEGRAM_WEBHOOK'] || '' 10 | this.interval = +process.env['TELEGRAM_INTERVAL'] || 500 11 | this.autoMarkdown = process.env['TELEGRAM_AUTO_MARKDOWN'] || 'enabled' 12 | this.offset = 0 13 | this.api = new Telegrambot(this.token) 14 | this.robot.logger.info(`Telegram Adapter Bot ${this.token} Loaded...`) 15 | // Get the bot information 16 | this.api.invoke('getMe', {}, (err, result) => { 17 | if (err) { 18 | this.emit('error', err) 19 | } else { 20 | this.bot_id = result.id 21 | this.bot_username = result.username 22 | this.bot_firstname = result.first_name 23 | this.robot.logger.info(`Telegram Bot Identified: ${this.bot_firstname}`) 24 | 25 | if (this.bot_username !== this.robot.name) { 26 | this.robot.name = this.bot_username 27 | } 28 | this.emit('connected') 29 | } 30 | }) 31 | } 32 | 33 | run () { 34 | if (!this.token) { 35 | this.emit('error', new Error('The environment variable "TELEGRAM_TOKEN" is required.')) 36 | } 37 | 38 | // Listen for Telegram API invokes from other scripts 39 | this.robot.on('telegram:invoke', (method, opts, cb) => this.api.invoke(method, opts, cb)) 40 | 41 | if (this.webhook) { 42 | const endpoint = this.webhook + '/' + this.token 43 | this.robot.logger.debug(`Listening on ${endpoint}`) 44 | 45 | this.api.invoke('setWebHook', { url: endpoint }, (err, result) => { 46 | if (err) { 47 | this.emit('error', err) 48 | } 49 | }) 50 | 51 | this.robot.router.post(`/${this.token}`, (req, res) => { 52 | if (req.body.message) { 53 | this.handleUpdate(req.body) 54 | } 55 | 56 | res.send('OK') 57 | }) 58 | } else { 59 | // Clear Webhook 60 | this.api.invoke('setWebHook', { url: '' }, (err, result) => { 61 | if (err) { 62 | this.emit('error', err) 63 | } 64 | }) 65 | this.getUpdate() 66 | } 67 | 68 | this.robot.logger.info('Telegram Adapter Started...') 69 | } 70 | 71 | cleanMessageText (text, chat_id) { 72 | // Private chat as mention 73 | // Fix hubot just check mentioned in message head. 74 | let mentionSign = `@${this.robot.name}` 75 | if (chat_id > 0 || text.match(mentionSign)) { 76 | text = `${mentionSign} ${text.replace(mentionSign, '')}` 77 | } 78 | return text 79 | } 80 | 81 | /** 82 | * Add extra options to the message packet before deliver. The extra options 83 | * will be pulled from the message envelope 84 | * 85 | * @param object message 86 | * @param object extra 87 | * 88 | * @return object 89 | */ 90 | applyExtraOptions (message, extra) { 91 | const { text } = message 92 | const detectMarkdown = /\*.+\*/.test(text) || /_.+_/.test(text) || /\[.+\]\(.+\)/.test(text) || /`.+`/.test(text) 93 | 94 | if (detectMarkdown && this.autoMarkdown === 'enabled') { 95 | message.parse_mode = 'Markdown' 96 | } 97 | 98 | if (extra != null) { 99 | for (let key in extra) { 100 | const value = extra[key] 101 | message[key] = value 102 | } 103 | } 104 | 105 | return message 106 | } 107 | 108 | /** 109 | * Get the last offset + 1, this will allow 110 | * the Telegram API to only return new relevant messages 111 | * 112 | * @return int 113 | */ 114 | getLastOffset () { 115 | return parseInt(this.offset) + 1 116 | } 117 | 118 | /** 119 | * Create a new user in relation with a chat_id 120 | * 121 | * @param object user 122 | * @param object chat 123 | * 124 | * @return object 125 | */ 126 | createUser (user, chat) { 127 | const opts = user 128 | opts.name = opts.username 129 | opts.room = String(chat.id) 130 | opts.telegram_chat = chat 131 | 132 | const result = this.robot.brain.userForId(user.id, opts) 133 | const current = result.first_name + result.last_name + result.username 134 | const update = user.first_name + user.last_name + user.username 135 | 136 | // Check for any changes, if the first or lastname updated...we will 137 | // user the new user object instead of the one from the brain 138 | if (current !== update) { 139 | this.robot.brain.data.users[user.id] = user 140 | this.robot.logger.info(`User ${user.id} regenerated. Persisting new user object.`) 141 | return user 142 | } 143 | 144 | return result 145 | } 146 | 147 | /** 148 | * Abstract send interaction with the Telegram API 149 | */ 150 | apiSend (opts, cb) { 151 | const chunks = opts.text.match(/[^]{1,4096}/g) 152 | 153 | this.robot.logger.debug(`Message length: ${opts.text.length}`) 154 | this.robot.logger.debug(`Message parts: ${chunks.length}`) 155 | 156 | // Chunk message delivery when required 157 | const send = cb => { 158 | if (chunks.length !== 0) { 159 | const current = chunks.shift() 160 | opts.text = current 161 | this.api.invoke('sendMessage', opts, (err, message) => { 162 | // Forward the callback to the original handler 163 | cb.apply(this, [err, message]) 164 | send(cb) 165 | }) 166 | } 167 | } 168 | 169 | // Start the recursive chunking cycle 170 | send(cb) 171 | } 172 | 173 | send (envelope, ...strings) { 174 | const text = strings.join() 175 | const data = this.applyExtraOptions({ chat_id: envelope.room, text }, envelope.telegram) 176 | this.apiSend(data, (err, message) => { 177 | if (err) { 178 | this.robot.logger.error(err) 179 | return 180 | } 181 | this.robot.logger.info(`Sending message to room: ${envelope.room}`) 182 | }) 183 | } 184 | 185 | reply (envelope, ...strings) { 186 | const text = strings.join() 187 | const data = this.applyExtraOptions({ 188 | chat_id: envelope.room, 189 | text, 190 | reply_to_message_id: envelope.message.id 191 | }, envelope.telegram) 192 | 193 | this.apiSend(data, (err, message) => { 194 | if (err) { 195 | this.robot.logger.error('error', err) 196 | return 197 | } 198 | this.robot.logger.info(`Reply message to room: ${envelope.room}`) 199 | }) 200 | } 201 | 202 | /** 203 | * "Private" method to handle a new update received via a webhook 204 | * or poll update. 205 | */ 206 | handleUpdate (update) { 207 | let text, user 208 | this.robot.logger.debug(update) 209 | 210 | const message = update.message || update.edited_message || update.callback_query 211 | this.robot.logger.info(`Receiving message_id: ${message.message_id}`) 212 | 213 | if (message.chat.type === 'group') groupsManager.update(message.chat.id, { name: message.chat.title }) 214 | 215 | if (this.robot.brain.get(`handled${message.message_id}`) === true) { 216 | this.robot.logger.warning(`Message ${message.message_id} already handled.`) 217 | return 218 | } 219 | this.robot.brain.set(`handled${message.message_id}`, true) 220 | 221 | // Text event 222 | if (message.text) { 223 | text = this.cleanMessageText(message.text, message.chat.id) 224 | this.robot.logger.info(`Received message: ${message.from.first_name} ${message.from.last_name} said '${text}'`) 225 | 226 | user = this.createUser(message.from, message.chat) 227 | this.receive(new TextMessage(user, text, message.message_id)) 228 | // Callback query 229 | } else if (message.data) { 230 | text = this.cleanMessageText(message.data, message.message.chat.id) 231 | 232 | this.robot.logger.debug(`Received callback query: ${message.from.username} said '${text}'`) 233 | 234 | user = this.createUser(message.from, message.message.chat) 235 | 236 | this.api.invoke('answerCallbackQuery', { callback_query_id: message.id }, function (err, result) { 237 | if (err) { 238 | this.robot.logger.error(err) 239 | } 240 | }) 241 | 242 | this.receive(new TextMessage(user, text, message.message.message_id)) 243 | 244 | // Join event 245 | } else if (message.new_chat_member) { 246 | user = this.createUser(message.new_chat_member, message.chat) 247 | this.robot.logger.info(`User ${user.id} joined chat ${message.chat.id}`) 248 | this.receive(new EnterMessage(user, null, message.message_id)) 249 | 250 | // Exit event 251 | } else if (message.left_chat_member) { 252 | user = this.createUser(message.left_chat_member, message.chat) 253 | this.robot.logger.info(`User ${user.id} left chat ${message.chat.id}`) 254 | this.receive(new LeaveMessage(user, null, message.message_id)) 255 | 256 | // Chat topic event 257 | } else if (message.new_chat_title) { 258 | user = this.createUser(message.from, message.chat) 259 | this.robot.logger.info(`User ${user.id} changed chat ${message.chat.id} title: ${message.new_chat_title}`) 260 | this.receive(new TopicMessage(user, message.new_chat_title, message.message_id)) 261 | } else { 262 | message.user = this.createUser(message.from, message.chat) 263 | this.receive(new CatchAllMessage(message)) 264 | } 265 | } 266 | 267 | getUpdate () { 268 | setTimeout(() => 269 | this.api.invoke('getUpdates', { offset: this.getLastOffset(), limit: 10 }, (err, result) => { 270 | if (err) { 271 | this.robot.logger.error('error', err) 272 | } else { 273 | if (result.length) { this.offset = result[result.length - 1].update_id } 274 | Array.from(result).map(msg => this.handleUpdate(msg)) 275 | } 276 | this.getUpdate() 277 | }), this.interval) 278 | } 279 | 280 | push (message, { rule, type = 'all', reg = true } = {}) { 281 | return new Promise((resolve, reject) => { 282 | let contacts = [] 283 | 284 | switch (type) { 285 | default: 286 | contacts = groupsManager.groups 287 | } 288 | 289 | if (rule) { 290 | let matcher = typeof rule === 'function' ? rule : roomName => { 291 | return reg ? roomName.match(rule) : roomName === rule 292 | } 293 | let _temp = [] 294 | contacts.map(contact => { 295 | if (matcher(contact.name)) { 296 | _temp.push(contact) 297 | } 298 | }) 299 | contacts = _temp 300 | } 301 | contacts.map(group => { 302 | this.robot.logger.info(`Push message to ${group.id} ${group.name}`) 303 | this.apiSend({ 304 | chat_id: group.id, 305 | text: message 306 | }, (err, msg) => { 307 | if (err) { 308 | console.log(err) 309 | } 310 | }) 311 | }) 312 | resolve() 313 | }) 314 | } 315 | } 316 | 317 | exports.use = robot => new TelegrambotAdapter(robot) 318 | --------------------------------------------------------------------------------