Телеграм бот обратной связи на Node.js и Telegraf.js

Простой Телеграм бот для связи с подписчиками и читателями на Node.js. Перепишем существующего бота, который ранее был написан на PHP.

Ранее мной был написан бот обратной связи на PHP, статья про него есть в ленте на сайте. Сейчас я практикуюсь в Node.js и решил переписать бот с использованием "Современного фреймворка для Телеграм Бот на Node.js" это Telegraf.js. Принцип работы бота остался тем же.

Не стал разбивать на отдельные файлы весь код, для наглядности оставил в одном листинге.

В настройках необходимо указать свои параметры, так как бот будет работать через webHook, то нужно указать путь до сертификатов для https соединения.

Ниже приведены 3 варианта с использованием бота через webHook и вариант через getUpdates

* * *

index.js - вариант 1

//////////////////////
////  Запускаем webHook
//////////////////////

// Подключаем модули
let fs = require('fs');
let Telegraf = require('telegraf');

/**
 * Настройки
 * @type token: string - Токен бота
 * @type path: string - относительный путь до директории с сертификатами 
 * @type key: string - приватный ключ 
 * @type cert: string - сертификат сервера
 * @type ca: string - сертификат клиента 
 * @type port: number - порт
 * @type domain: string - домен
 * @type whpath: string - путь
 * @type admin: number - id владельца бота
 */
let config = {
    "token": "YOUR_TOKEN",
    "path": "./certs/",
    "key": "file.key",
    "cert": "file.crt",
    "ca": "file.ca",
    "port": 8443,
    "domain": "domain.com",
    "whpath": "/secret-path",
    "admin": 123456789
};

// Создаем объект Telegraf.js
let bot = new Telegraf(config.token);

// TLS options
let tlsOptions = {
    key: fs.readFileSync(config.path + config.key),
    cert: fs.readFileSync(config.path + config.cert),
    ca: [
        // This is necessary only if the client uses the self-signed certificate.
        fs.readFileSync(config.path + config.ca)
    ]
};

// Установливаем webHook
bot.telegram.setWebhook('https://' + config.domain + ':' + config.port + config.whpath);

// Запускаем https webhook
bot.startWebhook(config.whpath, tlsOptions, config.port);

/**
 * Значения текстовых ответов
 * @type helloAdmin: string - Приветствие для админа
 * @type helloUser: string - Приветствие для пользователя
 * @type replyWrong: string - Если админ написал сам себе, уведомляем его
 */
let replyText = {
    "helloAdmin": "Привет админ, ждем сообщения от пользователей",
    "helloUser":  "Приветствую, отправьте мне сообщение. Постараюсь ответить в ближайшее время.",
    "replyWrong": "Для ответа пользователю используйте функцию Ответить/Reply."
};

/**
 * Проверяем пользователя на права
 * @param userId {number}
 * @returns {boolean}
 */
let isAdmin = (userId) => {
    return userId == config.admin;
};

/**
 *  Перенаправляем админу от пользователя или уведомляем админа об ошибке
 * @param ctx
 */
let forwardToAdmin = (ctx) => {
    if (isAdmin(ctx.message.from.id)) {
        ctx.reply(replyText.replyWrong);
    } else {
        ctx.forwardMessage(config.admin, ctx.from.id, ctx.message.id);
    }
};

/**
 * Старт бота
 */
bot.start((ctx) => {
    ctx.reply(isAdmin(ctx.message.from.id)
        ? replyText.helloAdmin
        : replyText.helloUser);
});


//////////////////////
////  Основа 1
//////////////////////

/**
 * Текст
 */
bot.on('text', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendMessage(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.text
        );
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * Стикер
 */
bot.on('sticker', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendSticker(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.sticker.file_id
        );
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * 1 фотография - не медиагруппа
 */
bot.on('photo', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        let file = ctx.message.photo.length - 1;
        ctx.telegram.sendPhoto(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.photo[file].file_id,
            {
                'caption': ctx.message.caption
            });
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * Документ
 */
bot.on('document', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendDocument(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.document.file_id,
            {
                'caption': ctx.message.caption
            });
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * Голосовая заметка
 */
bot.on('voice', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendVoice(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.voice.file_id,
            {
                'caption': ctx.message.caption
            });
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * Видео заметка
 */
bot.on('video_note', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendVideoNote(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.video_note.file_id
        );
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * 1 видео ролик - не медиагруппа
 */
bot.on('video', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendVideo(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.video.file_id,
            {
                'caption': ctx.message.caption
            }
        );
    } else {
        forwardToAdmin(ctx);
    }
});

/**
 * Аудио ролик
 */
bot.on('audio', (ctx) => {
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        ctx.telegram.sendAudio(
            ctx.message.reply_to_message.forward_from.id,
            ctx.message.audio.file_id,
            {
                'caption': ctx.message.caption
            });
    } else {
        forwardToAdmin(ctx);
    }
});

* * *

index.js - вариант 2

Упрощаем код, ставим прослушку на общий метод Message. В соответствии с подтипом сообщения вызывая нужный метод отправляем сообщение пользователю.

//////////////////////
////  ... Здесь запускаем webHook из первого варианта
//////////////////////

//////////////////////
////  Основа 2
//////////////////////
/**
 * Слушаем на наличие объекта message
 */
bot.on('message', (ctx) => {
    // убеждаемся что это админ ответил на сообщение пользователя
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        // кому отправляем
        let userId = ctx.message.reply_to_message.forward_from.id;
        //  проверяем что пришло и отправляем соответствующим методом
        switch (ctx.updateSubTypes[0]) {
            case 'text':
                ctx.telegram.sendMessage(
                    userId,
                    ctx.message.text
                );
                break;
            case 'sticker':
                ctx.telegram.sendSticker(
                    userId,
                    ctx.message.sticker.file_id
                );
                break;
            case 'photo':
                let file = ctx.message.photo.length - 1;
                ctx.telegram.sendPhoto(
                    userId,
                    ctx.message.photo[file].file_id,
                    {
                        'caption': ctx.message.caption
                    }
                );
                break;
            case 'document':
                ctx.telegram.sendDocument(
                    userId,
                    ctx.message.document.file_id,
                    {
                        'caption': ctx.message.caption
                    }
                );
                break;
            case 'voice':
                ctx.telegram.sendVoice(
                    userId,
                    ctx.message.voice.file_id,
                    {
                        'caption': ctx.message.caption
                    }
                );
                break;
            case 'video_note':
                ctx.telegram.sendVideoNote(
                    userId,
                    ctx.message.video_note.file_id
                );
                break;
            case 'video':
                ctx.telegram.sendVideo(
                    userId,
                    ctx.message.video.file_id,
                    {
                        'caption': ctx.message.caption
                    }
                );
                break;
            case 'audio':
                ctx.telegram.sendAudio(
                    userId,
                    ctx.message.audio.file_id,
                    {
                        'caption': ctx.message.caption
                    }
                );
                break;
            default:
                console.log('other');
       }
    } else {
        forwardToAdmin(ctx);
    }
});

* * *

index.js - вариант 3

Максимально упростим код и используя метод sendCopy - просто отправляем копию сообщения от админа пользователю.

//////////////////////
////  ... Здесь запускаем webHook из первого варианта
//////////////////////

//////////////////////
////  Основа 3
//////////////////////
/**
 * Слушаем на наличие объекта message
 */
bot.on('message', (ctx) => {
    // убеждаемся что это админ ответил на сообщение пользователя
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        // отправляем копию пользователю
        ctx.telegram.sendCopy(ctx.message.reply_to_message.forward_from.id, ctx.message);
    } else {
        // перенаправляем админу
        forwardToAdmin(ctx);
    }
});

* * *

Вариант бота без webHook 

Этот вариант можно запустить без настройки webHook, также не нужны домен и ssl-сертификат. Его можно спокойно запустить на локальной машине, при необходимости можно настроить соединение через прокси.

Файл index.js

// Подключаем модули
const Telegraf = require('telegraf');
// const HttpsProxyAgent = require('https-proxy-agent');
// Общие настройки
let config = {
    "token": "YOUR_TOKEN", // Токен бота
    "admin": 0 // id владельца бота
};
// Создаем объект бота
const bot = new Telegraf(config.token, {
        // Если надо ходить через прокси - укажите: user, pass, host, port
        // telegram: { agent: new HttpsProxyAgent('http://user:pass@host:port') }
    }
);
// Текстовые настройки
let replyText = {
    "helloAdmin": "Привет админ, ждем сообщения от пользователей",
    "helloUser":  "Приветствую, отправьте мне сообщение. Постараюсь ответить в ближайшее время.",
    "replyWrong": "Для ответа пользователю используйте функцию Ответить/Reply."
};
// Проверяем пользователя на права
let isAdmin = (userId) => {
    return userId == config.admin;
};
// Перенаправляем админу от пользователя или уведомляем админа об ошибке
let forwardToAdmin = (ctx) => {
    if (isAdmin(ctx.message.from.id)) {
        ctx.reply(replyText.replyWrong);
    } else {
        ctx.forwardMessage(config.admin, ctx.from.id, ctx.message.id);
    }
};
// Старт бота
bot.start((ctx) => {
    ctx.reply(isAdmin(ctx.message.from.id)
        ? replyText.helloAdmin
        : replyText.helloUser);
});
// Слушаем на наличие объекта message
bot.on('message', (ctx) => {
    // убеждаемся что это админ ответил на сообщение пользователя
    if (ctx.message.reply_to_message
        && ctx.message.reply_to_message.forward_from
        && isAdmin(ctx.message.from.id)) {
        // отправляем копию пользователю
        ctx.telegram.sendCopy(ctx.message.reply_to_message.forward_from.id, ctx.message);
    } else {
        // перенаправляем админу
        forwardToAdmin(ctx);
    }
});
// запускаем бот
bot.launch();

* * *

Файл package.json

{
  "name": "telegramFeedBack",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "iMakeBots.ru",
  "license": "",
  "dependencies": {
    "https-proxy-agent": "^2.2.1",
    "telegraf": "^3.26.0"
  }
}
12 комментариев
Авторизуйтесь через Telegram, чтобы оставить комментарий.
Откройте по ссылке или QR бот @iMakeBot, нажмите кнопку Старт/Start.
Следуйте инструкциям бота.

  • Nick Dornik ★ [4 года назад]

    Код написан на телеграфе, а хотелось бы на чистом Node.JS

  • iMakeBots [4 года назад → Nick Dornik ★]

    Думаю, что это не сложно реализовать.
    Будет время, для себя и для эксперимента попробую).

  • Игорь [3 года назад]

    Не совсем понял как именно ответить на полученное сообщение от пользователя. На сообщении нажимаю "Ответить", вписываю свой ответ, но бот не перенаправляет от админа к пользователю, а выдаёт ошибку replyWrong.

    Подскажите, какой порядок действий для перенаправления ответа пользователю?

  • iMakeBots [3 года назад → Игорь]

    У вас закрыты в настройках возможность перейти в профиль при пересылаемых сообщениях?

  • Игорь [3 года назад → iMakeBots]

    Да, вы оказались правы.

    Проблема была в том, что у юзера была отключена "пересылка сообщений" т.е. по его имени нельзя было открыть его профиль.
    Так что для внедрения функции обратной связи нужно или попросить юзера включить пересылку сообщений, или придумать другой способ ответа на сообщение.

    Но и без этого автору крайне благодарен за подробную инструкцию!

  • iMakeBots [3 года назад → Игорь]

    Решение есть и оно на самом деле очень простое, на php у меня оно реализовано.

    При получении сообщения бот видит id отправителя, раньше я его просто пересылал - пересылка норм проходит, но при попытке ответить если у пользователя скрыт профиль при пересылке, то была ошибка, это связано с тем что в пересланном сообщении нет id пользователя. Выход в не пересылке сообщения, в отправке копии - я его создаю заново и в тело сообщения указываю в начале id - далее при ответе парсю текст вытаскиваю id и шлю ответ.

  • Максим Решетов [3 года назад]

    Отлично работает в приват чате с админом, но как организовать перессылку сообщений из приват канал?

    Поменял id admin на id чата - сообщения пользователей прилетают в чат, но при ответе на них ничего не происходит.

    Пользуюсь кодом без вебхук.

  • iMakeBots [3 года назад → Максим Решетов]

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

  • inclusion framework [2 года назад]

    что такое whpath?

  • inclusion framework [2 года назад → inclusion framework]

    заработало с whpath по-умолчанию (/secret-path)

  • Max [2 года назад]

    когда пользователем/админом закрыта возможность перейти в профиль при пересылаемых сообщениях ответы от админа не приходит

    есть наглядное решение проблем на js ?

  • iMakeBots [1 год назад → Max]

    Пример есть в новой статье