Рецепт разработки бота под Telegram

e3fe5229e32b49b9b296b524fd8e8ccc.png

Добрый день, уважаемые читатели Хабрахабра!

В этом топике я хочу поделиться с вами опытом разработки бота под Telegram за 4 дня. Этот бот переводит все голосовые сообщения, которые получает, в текст. Пытался сделать быстро, но качественно — подучил пару-тройку технологий. Постараюсь максимально подробно описать свой процесс преодоления ошибок и преград; доказать, что, даже не имея нужных навыков, запустить свой продукт не так-то и сложно.

Статья может быть интересна как новичкам в программировании — увидеть, сколько препятствий стоят на пути у готового продукта, так и более продвинутым специалистам — где-то посмеяться, где-то поплакать, где-то написать комментарий «жизненно».

Преамбула


И так, что же может сделать один программист за 4 дня?

Написать модуль, написать один экран iOS приложения, написать 15% сложного бота под Telegram, поднять или опустить MMR в Dota2 или ранг в Overwatch. Может провести выходные на природе, сходить в тренажерный зал, поплавать в бассейне, написать 500–1000 строк кода в приложении для клиентов, которым никогда и никто, кроме друзей и родственников заказчика, пользоваться не будет. Может смотаться в Кремниевую Долину на последние деньги, посмотреть пару-тройку сериалов, познакомиться с уймой людей на одной-двух конференциях, повтыкать в документацию Ассемблера.

А что, если я скажу, что все это — полная чушь? Что, если я вам открою секрет: все не так-то просто. Время — штука относительная, можно мерить ее разными величинами и коэффициентами; мой любимый критерий — отношение времени к пользе. Что, если я вам скажу, что программист может запустить новый продукт на рынок за 4 дня? Заинтригованы? — Читайте дальше.

Мотивация


К нам в сообщество программистов в Telegram недавно добавился «наш собственный ивангай» и начал дико сорить направо и налево голосовыми сообщениями (кто его знает, вдруг парню просто сложно писать пальцами по клавиатуре). Естественно, людям надоело слушать по два-три часа в сутки сообщения, чтобы вникнуть в суть беседы — и тут кто-то предложил создать бота, который автоматически переводил бы все войсы в текст. Это именно тот момент, когда загораются глаза.

Перевод голоса в текст


Я не знал, сколько времени займет у меня разработка подобного бота, но уже имел достаточно опыта за плечами с ботами из нашего теплого уютного чатика и фриланс-биржой в Telegram с открытым исходным кодом. Не долго думая, сел за разработку — этап подключения модулей Telegam Bot API прошел, как по маслу. Что дальше? Модуль распознавания речи — гуглим «voice recognition api» и получаем среди первых ссылок список лучших сервисов. Во главе стоит Google Speech API в бета-версии — его и возьмем.

Получаем все голосовые сообщения ботом и как-то направляем в сторону Google. Но, вот незадача: почти все npm модули работы с этим сервисом либо устарели, либо банально не работают. Пробуем использовать встроенное API под Node.js, написанное специалистами Google — работает. Что там по аутентификации? Испробовав уйму вариантов из документации от Google — которая, кстати говоря, тоже 50 на 50 устарела или не работает — и вот оно, ключики подошли.

К слову, вот сам код, который я использовал:

Жми меня!
const Speech = require('@google-cloud/speech');

const speech = new Speech({
  projectId: 'voicy-151205',
  credentials: require('path/to/certificate/file.json')
});

speech.startRecognition(filepath, {
  'encoding': 'LINEAR16',
  'sampleRate': 16000,
  'languageCode': 'en-US',
})
  .then((results) => {
    const operation = results[0];
    return operation.promise();
  })
  .then((transcription) => {
    console.log(transcription[0]);
  })

Аудио форматы и ограничения Google


Что там по отправке войсов к Google? Ага, нужно получить аудиофайлы из Telegram и отправлять их на сервера Speech API — пробуем, не работает. В чем дело? Формат .oga, обычный для Telegram, не принимается — нужно декодировать. Что у нас есть для конвертации медиа? Конечно же, ffmpeg, знакомый еще с раннего детства (спасибо папе, который когда-то давно заставил меня в нем разбираться). Как его подключить к ноде? Опа! Есть npm модуль, заточенный специально под это. В какой формат нужно конвертировать .oga файлы? Оказалось, Google принимает .flac — конвертируем, пробуем, все принимается, Google отвечает текстом, успех.

Но, не тут-то было! Google не переводит в текст файлы длиннее 60 секунд, если не загружать их на Google Cloud Storage сервис. Что же такое? Сразу же пробуем модуль, написанный программистами Google, даже не смотрим в сторону устаревших npm готовых решений. Отлично, файлы загружаются, обрабатываются сервисом, возвращается текст. Кстати, странное у Google понятие времени — аудиофайлы длинной в 30 секунд, почему-то, определяются, как 60-ти секундные. Ничего страшного — пробуем файл длиннее 30 секунд и наступаем на очередные грабли — Google принимает длинные файлы только в кодировке «LINEAR16».

Что еще за «LINEAR16»? Ищем в документации ffmpeg — ничего подобного нет. Отлично, как так? Работаем дальше. Ищем, что за зверь этот формат или кодек — оказывается, это »16 bit signed little endian» данные. Хорошо, что я прочитал пару книжек по Computer Science и знаю, что такое »16 bit» и почему это «little endian». Ищем, что же из себя представляет этот формат в ffmpeg — ага: «s16le»! Пробуем конвертировать — получается, Google принимает и отвечает текстом, «Mission accomplished».

Осторожно: ниже код, который помог мне с конвертацией (нужно предустановить ffmpeg на машину)!

Жми меня!
const ffmpeg = require('fluent-ffmpeg');
const temp = require('temp');

ffmpeg.ffprobe(filepath, (err, info) => {
  const fileSize = info.format.duration;
  const output = temp.path({ suffix: '.flac' });

  ffmpeg()
    .on('end', () => console.log(output))
    .input(filepath)
    .setStartTime(0)
    .duration(fileSize)
    .output(output)
    .audioFrequency(16000)
    .toFormat('s16le')
    .run();
});

Монетизация


Но, что это такое? Какие $2 за использование Speech API? Они там в Google совсем с дубу рухнули? Как это мы использовали более двух часов перевода голоса в текст? Так совсем не пойдет — добавят моего бота в 100–1000 чатов, и что мне потом делать? С завтраков сэкономить на поддержку бота уже не получится — не тот масштаб. Нужно как-то прикручивать оплату. Пускай, будет 600 бесплатных секунд у каждого чата, а дальше будем запрашивать покрытие стоимости Google Speech API.

Как прикрутить оплату к боту в Telegram? Четких инструкций, как и инструментов монетизации в Telegram еще не завезли — нужно как-то решать вопрос вовне. Какой платежный сервис использовать? Так-так-так, недавно читал про парня, который стал самым молодым миллиардером — тот создал свой платежный сервис. Гуглим, видим — Stripe, его и используем. Что это у нас? У них есть удобный Checkout —, но стандартную форму мы, конечно, использовать не будем, сделаем свою.

Фронтенд


Куда смотреть, чтобы делать интерактивные странички? Я же iOS программист, недавно окунулся в серверную разработку — так, ноги слегка сполоснуть —, а тут фронтенд подоспел, что же за напасть? Гуглим быстренько, какие технологии используются для создания интерактивных сайтов. Angular 2, jQuery, Vanila.js — самым простым выглядит jQuery, его и возьмем — тем более, я с ним когда-то давно уже развлекался. Гуглим YouTube уроки по jQuery — один трешак по 2–3 часа, будем разбираться по ходу дела, туториалам и ответам на Stack Overflow.

Быстренько рисуем структуру сайта — лого, форму, пару строк текста и кнопку. Как это сделать? Что-то я слышал про Bootstrap. Гуглим, проходим пару уроков по Bootstrap 3 — еще гуглим, прикручиваем картинку, форму, кнопочку — вроде как, даже адаптивно получилось. Меняем фон сайта с белого на что-то более креативное (сероватый слегка), вот и сайтик готов.

Время jQuery! Решаем вынести оплату в отдельный проект — запускаем в командной строке «express payments», получаем проект. Отлично, куда заносить скрипты? Как, прямо в сайт? Что-то не так — и вправду, можно вынести их в отдельный файл, этим и займемся. Прикручиваем обработку ошибок на отдельный лейбл, проверяем форму Stripe (благо, они дают весь нужный пользовательский интерфейс в Checkout модуле), прикручиваем наш новый проект к той же базе данных, проверяем оплату — проходит, все работает. По пути, конечно, ввиду отсутствия привычным фронтенд-разработчикам инструментов, мириады раз перезагружаем веб-страничку вручную.

Вот тут файлики, которые у меня получились (осторожно, не очень чистый код):

Жми меня!
index.hjs


  
    Voicy payments
    
    
    
    
    
    
  
  
    Voicy
    

Chat ID:

{{ chatId }}

{{ seconds }} seconds are left in this chat.

You can buy more seconds below.

$0.4 per 200 seconds


global.js
$(document).ready(function() {
  var chatId;
  var amount;

  var handler = StripeCheckout.configure({
    key: '***',
    image: 'https://pay.voicybot.com/images/stripe.png',
    locale: 'auto',
    // alipay: true,
    // bitcoin: true,
    closed: function() {
      $("#successLabel").empty();
      $("#errorLabel").empty();
    },
    token: function(token) {
      $("#infoLabel").empty();
      $("#successLabel").empty();
      $("#errorLabel").empty();
      $("#infoLabel").append('Processing payment on Voicy servers...');
      $.ajax({
        type: 'POST',
        url: 'buy',
        data: { 'token': token.id, 'chatId': chatId, 'amount': amount },
        dataType: 'json',
        encode: true
      })
      .done(function(data) {
        if (data['error']) {
          $("#infoLabel").empty();
          $("#successLabel").empty();
          $("#errorLabel").empty();
          $("#errorLabel").append(data['error']);
        } else {
          $("#infoLabel").empty();
          $("#successLabel").empty();
          $("#errorLabel").empty();
          $("#successLabel").append('Thank you for the payment!');
        }
      });
    }
  });

  // Close Checkout on page navigation:
  window.addEventListener('popstate', function() {
    $("#infoLabel").empty();
    $("#successLabel").empty();
    $("#errorLabel").empty();
    handler.close();
  });

  // process the form
  $('form').submit(function(event) {
    event.preventDefault();
    var seconds = $('input[name=numberOfSeconds]').val();
    chatId = $('input[name=chatId]').val();
    if (!seconds || seconds < 200) {
      $("#infoLabel").empty();
      $("#successLabel").empty();
      $("#errorLabel").empty();
      $("#errorLabel").append('Please purchase at least 200 seconds');
    } else {
      var purch = seconds * 0.002 * 100;
      amount = seconds;
      $("#infoLabel").empty();
      $("#successLabel").empty();
      $("#errorLabel").empty();
      $("#infoLabel").append('Please pay at Stripe Checkout');
      handler.open({
        name: 'Voicy Bot',
        description: 'Purchasing ' + seconds + ' seconds',
        currency: 'USD',
        amount: purch,
        // alipay: true,
        // bitcoin: true
      });
    }
  });
});

index.js
const express = require('express');
const router = express.Router();
const db = require('../helpers/db');
const stripe = require("stripe")("***");
const bot = require('../helpers/bot');

/** Process purchase */
router.post('/buy', (req, res, next) => {
  const token = req.body.token;
  const chatId = parseInt(req.body.chatId);
  const amount = parseInt(req.body.amount);

  var charge = stripe.charges.create({
    amount: amount * 0.002 * 100,
    source: token,
    currency: "USD",
    description: "Buying seconds for Voicy"
  }, (err, charge) => {
    if (err) {
      res.send({ error: err.message });
    } else {
      db.findChat(chatId)
        .then((chat) => {
          chat.seconds = parseInt(chat.seconds) + amount;
          return chat.save()
            .then((newChat) => {
              res.send({ success: true });
              reportPaymentToChat(newChat, amount);
            });
        })
        .catch((err) => {
          res.send({ error: err.message });
        })
    }
  });
});

/* GET home page. */
router.get('/:id', (req, res, next) => {
  const chatId = parseInt(req.params.id);
  db.findChat(chatId)
    .then((chat) => {
      if (!chat) {
        const err = new Error();
        err.status = 404;
        err.message = 'No chat found';
        throw err;
      }
      return chat;
    })
    .then((chat) => {
      res.render('index', { 
        chatId: chat.id,
        seconds: chat.seconds,
      });
    })
    .catch(err => next(err));
});


Главный вебсайт


Любому хорошему проекту нужен добротный вебсайт —, но мне что-то совсем не хочется тратить деньги на хостинг. Покупаем доменное имя и направляем его прямо на GitHub Pages, благо, с этим опыта достаточно с предыдущих проектов с открытым кодом. Берем стандартный шаблон, заполняем его нужными данными, слегка модифицируем index.html — готово.

Nginx и SSL


Но как мне будут доверять пользователи, если форма оплаты будет недоступна по https? Ничего страшного — плавали, знаем! Запускаем CertBot на сервере, получаем нужные SSL сертификаты. Пока что приложение доступно на pay.*domain*.com:3000 — негоже сюда направлять пользователей. Настраиваем Nginx, чтобы он перенаправлял все реквесты с http на https и вешаем прокси с 80 порта на 3000 — проверяем, заходим на сайт, работает.

Если кому интересно, вот файл настроек nginx:

Жми меня!
# HTTP - redirect all requests to HTTPS:
server {
        listen 80;
        listen [::]:80 default_server ipv6only=on;
        return 301 https://$host$request_uri;
}

# HTTPS - proxy requests on to local Node.js app:
server {
        listen 443;
        server_name your_domain_name;

       ssl on;
        # Use certificate and key provided by Lets Encrypt:
        ssl_certificate /etc/letsencrypt/live/pay.voicybot.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/pay.voicybot.com/privkey.pem;
        ssl_session_timeout 5m;
        ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
        ssl_prefer_server_ciphers on;
        ssl_ciphers '***';
        # Pass requests for / to localhost:3001:
        location / {
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-NginX-Proxy true;
                proxy_pass http://localhost:3000/;
                proxy_ssl_session_reuse off;
                proxy_set_header Host $http_host;
                proxy_cache_bypass $http_upgrade;
                proxy_redirect off;
        }
}

Заключение


Вот так неожиданно и оборвался мой рассказ — так как все оказалось готово: один сервер принимает все голосовые сообщения и переводит их в текст, второй отвечает за безопасную оплату времени обработки войсов, а GitHub раздает головной сайт проекта. Главное, что я хотел описать в этой статье — это то, что процесс создания продукта хоть и трудоемкий, но может уместиться в 40–80 часов благодаря уже существующим инструментам в Интернете.

Напомню, что этот маленький проект включает в себя:

  • Нейронную сеть от Google, конвертирующую голос в текст
  • Неплохой вебсайт, созданный при неимении каких-либо дизайн или фронтенд навыков
  • Умный инструмент оплаты, который, к слову, работает во всех браузерах и даже с AliPay да Биткоинами
  • Базу данных чатов и пользователей, по которой всегда можно глянуть статистику
  • Конвертацию аудио файлов между разными форматами
  • Сложный сервер с SSL, с двумя поднятыми комплексными приложениями
  • Отсутствие какого-либо человеческого фактора: все работает автоматически

Если вы все еще думаете, что для создания собственного продукта у вас недостаточно навыков — оглянитесь вокруг, все уже сделано за вас. В отличие от того же 2000, сегодня создание продукта не требует глубокого знания алгоритмов и структур данных. Так чего же вы ждете?

Благодарности


Огромное спасибо, что дочитали до конца! Я с радостью отвечу на все комментарии к статье. Ссылку на сам бот не прилагаю (хоть он и полностью рабочий), так как это против правил Хабрахабра.

Rock on.

Комментарии (6)

  • 5 декабря 2016 в 10:33

    +2

    Хотелось бы больше подробностей. Примеров кода по конвертации через ffmpeg, работу с Google Cloud Storage. Сейчас больше похоже на главу из очередной нон-фикшн книги с названием: «Бери от жизни все! Да, ты сможешь».

    И где можно бота в действии посмотреть-то?

    • 5 декабря 2016 в 10:42

      +1

      Добрый день! Постараюсь учесть ваш комментарий и дополнить статью примерами кода.

      К сожалению, боюсь, меня с Хабрахабра выпилят за прямую ссылку на бота.

    • 5 декабря 2016 в 11:11

      +1

      долго искал, вот ссылка https://telegram.me/voicybot
  • 5 декабря 2016 в 10:36

    +1

    Без кода или ссылки на исходники не вижу смысла в упоминании JavaScrtipt и Node.js.
    То же самое могу сказать про слово «успех». Вы что то заработали на боте или пока только «успех»?
    • 5 декабря 2016 в 10:40 (комментарий был изменён)

      +1

      Большое спасибо за комментарий! Бот написан на Node.js — в статье попутно указываю фреймворки, которые использовал, в частности, от Google. Обязательно учту ваш комментарий — добавляю примеры кода в статью прямо сейчас.

      Поискал слово «Успех» в своем тексте — нашел:

      Оказалось, Google принимает .flac — конвертируем, пробуем, все принимается, Google отвечает текстом, успех.

      Да, Google ответил текстом — вот он, мой «успех».
  • 5 декабря 2016 в 10:45

    0

    image

© Habrahabr.ru