Так уж вышло, что род моей деятельности тесно переплетен с созданием ботов для Telegram. Писать я их начал сразу после появления Telegram Bot API, тогда никаких инструментов для этого не было. Пришлось самому писать библиотеку для работы с API, о чем я частично уже рассказывал в своей предыдущей статье. С течением времени библиотека несколько раз была переписана и в итоге обросла разными фишками. В статье я постараюсь рассказать о том, как с ее помощью писать ботов.





Для работы с API прежде всего понадобится token, чтобы его получить достаточно написать этому боту и проследовать его инструкциям.

Начну сразу с примера простого бота:

'use strict'

var tg = require('telegram-node-bot')('YOUR_TOKEN')

tg.router.
    when(['ping'], 'PingController')

tg.controller('PingController', ($) => {
    tg.for('ping', () => {
        $.sendMessage('pong')
    })
}) 


Работает!



А теперь разберем что там написано:

var tg = require('telegram-node-bot')('YOUR_TOKEN') 

Тут все понятно, просто объявляем модуль и передаем ему наш токен.

tg.router.
    when(['ping'], 'PingController')

Далее мы объявляем команды и роутеры, которые отвечают за эти команды.

tg.controller('PingController', ($) => {
    tg.for('ping', () => {
        $.sendMessage('pong')
    })
}) 

После мы создаем контроллер «PingController» и объявляем в нем обработчик команды ping.

Когда бот получит команду от пользователя, он поймет, что за команду ping отвечает контроллер «PingController» и исполнит обработчик команды ping.

На этом можно закрыть статью и начать писать простых ботов, а я продолжу рассказывать.

Роутер


Как я уже писал выше, роутер отвечает за связь команд и контроллеров, которые эти команды обрабатывают.
Очевидно, что один контроллер может обрабатывать несколько команд.
Так же у роутера есть функция otherwise, с помощью которой можно объявить контроллер, который будет обрабатывать непредусмотренные команды:

tg.router.
    when(['test', 'test2'], 'TestController'). // "TestController" будет обрабатывать как команду test, так и команду test2
    when(['ping'], 'PingController'). 
    otherwise('OtherController') //"OtherController" будет вызван для всех остальных команд.



Контроллер


Когда бот получил команду от пользователя в игру вступают контроллеры. Как видно из примера выше — контроллеры обрабатывают команды пользователя. В рамках одного контроллера можно обрабатывать несколько команд:

tg.controller('TestController', ($) => {
    tg.for('test', () => {
        $.sendMessage('test')
    })
    tg.for('test2', () => {
        $.sendMessage('test2')
    })
}) 

В контроллере также можно писать любые свои функции, переменные.

Scope


Каждый контроллер принимает особую переменную $ — scope.
В ней хранится все что нам нужно знать о запросе:


  • chatId — id чата, откуда пришел запрос
  • user — информация о пользователе, который отправил запрос, подробнее тут
  • message — вся информация о сообщении, подробнее
  • args — сообщение, которое отправил пользователь, но без самой команды. (Если пользователь отправит "/start 1" в args будет — «1»)


Вы могли заметить, что, например, функцию sendMessage мы вызывали с помощью scope. Дело в том, что помимо полей описанных выше, scope также содержит все функции библиотеки с уже прописанным chatId для конкретного чата. Ту же функцию sendMessage можно вызвать напрямую:

tg.controller('TestController', ($) => {
    tg.for('test', () => {
        tg.sendMessage($.chatId, 'test')
    })
    tg.for('test2', () => {
        tg.sendMessage($.chatId, 'test2')
    })
}) 

Согласитесь, не очень удобно писать id чата, учитывая, что мы пишем в тот же чат, откуда пришло сообщение.

Цепочка вызовов


Иногда мы что-то спрашиваем у пользователя и ждем от него какой-либо информации. Как это реализовать? Конечно, мы можем хранить состояние пользователей и в зависимости от него обрабатывать это в «OtherController», но это не совсем красиво, и ломает структуру и читаемость.
Для таких случаев у scope есть функция waitForRequest:
tg.controller('TestController', ($) => {
    tg.for('/reg', ($) => {
         $.sendMessage('Send me your name!')
         $.waitForRequest(($) => {
             $.sendMessage('Hi ' + $.message.text + '!')
         }) 
    })   
}) 

Функция waitForRequest принимает один аргумент — callback, который она вызовет когда пользователь отправит следующее сообщение. В этот callback передается новый scope. Как видно из примера выше — мы просим пользователя ввести его имя, ждем его следующее сообщение и приветствуем его.

Навигация



Допустим у нашего бота есть авторизация, в главном контроллере нам нам нужно как-то проверять авторизован ли пользователь и перенаправлять его в логин, но как? Для этого есть функция routeTo, которая принимает команду и выполняет ее как если бы ее отправил нам пользователь:
tg.controller('StartController', ($) => {
    tg.for('/profile', ($) => {
        if(!logined){ // какая то логика авторизации
            $.routeTo("/login") // перенаправляем пользователя
        }       
    }) 
})


Формы


Часто бывает, что нужно узнать у пользователя какую-то информацию, для этого есть генератор форм:
var form = {
    name: {
        q: 'Send me your name',
        error: 'sorry, wrong input',
        validator: (input, callback) => {
            if(input['text']) {
                callback(true)
                return
            }

            callback(false)
        }
    },
    age: {
        q: 'Send me your age',
        error: 'sorry, wrong input',
        validator: (input, callback) => {
            if(input['text'] && IsNumeric(input['text'])) {
                callback(true)
                return
            }

            callback(false)
        }
    },
    sex: {
        q: 'Select your sex',
        keyboard: [['male'],['famale'], ['UFO']],
        error: 'sorry, wrong input',
        validator: (input, callback) => {
            if(input['text'] && ['male', 'famale', 'UFO'].indexOf(input['text']) > -1) {
                callback(true)
                return
            }

            callback(false)
        }
    },         
}

$.runForm(form, (result) => {
    console.log(result)
})  

Как видно из кода у каждого поля есть сообщение отравляемое пользователю, валидатор, в который приходит сообщение от пользователя и сообщение об ошибке в случае неверности вводимых данных. Также можно отправить клавиатуру (keyboard).
Функция runForm вернет нам объект с теми же полями какие мы написали в самой форме, в нашем случае это name и age.

Меню



Похожий инструмент есть и для меню:
$.runMenu({
    message: 'Select:',
    'Exit': {
        message: 'Do you realy want to exit?',
        'yes': () => {

        },
        'no': () => {

        }
    } 
})  

Меню автоматически создает клавиатуру с названиями полей и отправляет ее вместе с сообщением, которое мы указываем в поле message.
Пункт меню может быть как объектом так и функцией. Если это объект, то пользователь получит подменю, а если функция, то она будет вызвана и позволит нам обработать запрос пользователя.

Также есть возможность задавать расположение кнопок в клавиатуре меню, за это отвечает поле layout, если его ��е передавать совсем, то на каждой строке будет одна кнопка. Можно передать максимальное число кнопок на строке или массив количества кнопок для каждой строки:
$.runMenu({
    message: 'Select:',
    layout: 2,
    'test1': () => {}, //будет на первой строке
    'test2': () => {}, //будет на первой строке
    'test3': () => {}, //будет на второй строке
    'test4': () => {}, //будет на третей строке
    'test5': () => {}, //будет на четвертой строке
})  


$.runMenu({
    message: 'Select:',
    layout: [1, 2, 1, 1],
    'test1': () => {}, //будет на первой строке
    'test2': () => {}, //будет на второй строке
    'test3': () => {}, //будет на второй строке
    'test4': () => {}, //будет на третей строке
    'test5': () => {}, ///будет на четвертой строке
}) 


Функции API


У всеж функций для работы с API есть как обязательные параметры, так и необязательные. Например функция sendMessage — по документации у нее для обязательных параметра (chatId, photo), но мы можем передать и любые дополнительные:
var options = {
     reply_markup: JSON.stringify({
         one_time_keyboard: true,
          keyboard: [['test']]
    })
}	            
$.sendMessage('test', options)


При этом последним параметром всегда является callback.

Вот список поддерживаемых на данный момент функций API с обязательными параметрами (напомню, что если вызывать их из scope, то параметр chatId не нужен):

  • sendPhoto(chatId, photo)
  • sendDocument(chatId, document)
  • sendMessage(chatId, text)
  • sendLocation(chatId, latitude, longitude)
  • sendAudio(chatId, audio)
  • forwardMessage(chatId, fromChatId, messageId)
  • getFile(fileId)
  • sendChatAction(chatId, action)
  • getUserProfilePhotos(userId)
  • sendSticker(chatId, sticker)
  • sendVoice(chatId, voice)
  • sendVideo(chatId, video)


Примеры вызова некоторых функций:
var doc =  {
    value: fs.createReadStream('file.png'), //stream
    filename: 'photo.png',
    contentType: 'image/png'
}

$.sendDocument(doc)

$.sendPhoto(fs.createReadStream('photo.jpeg'))

$.sendAudio(fs.createReadStream('audio.mp3'))

$.sendVoice(fs.createReadStream('voice.ogg'))

$.sendVideo(fs.createReadStream('video.mp4'))

$.sendSticker(fs.createReadStream('sticker.webp'))



На этом все, спасибо всем кто осилил статью, а вот и ссылка на GitHub — github.com/Naltox/telegram-node-bot и NPM: npmjs.com/package/telegram-node-bot