Создание и развертывание ретранслятора Telegram каналов, используя Python и Heroku
В данной инструкции построим Python приложение для зеркалирования сообщений из Telegram каналов и развернем его на облачной платформе Heroku.
Подготовка
Идея проста — получить сообщение и тут же переотправить или переслать его.
Ввиду ограниченности Bot API, нет возможности добавить бота в канал, где вы не администратор. Поэтому будем использовать клиентский API, заходя на нужные каналы вручную (хотя и этот шаг можно автоматизировать).
По ходу действий нам понадобятся:
Аккаунт Telegram. Лучше перестраховаться и завести отдельный аккаунт, благо на сервисах приема SMS его цена составляет ~6₽. Для таких аккаунтов лучше сразу ставить двухфакторную аутентификацию;
Созданный Telegram канал, куда будем отправлять сообщения;
Аккаунт на облачной платформе Heroku. Регистрация.
Heroku CLI для взаимодейсвия с платформой Heroku. Установка.
Взаимодействие с Telegram API
Для работы с клиентским API необходимо создать приложение Telegram. Сделать это можно по ссылке. Здесь нас интересуют два значения: api_id и api_hash.
Полученные значения api_id и api_hash занесем во вновь созданный файл переменных окружения .env:
Создание Python приложения
Создадим папку проекта. Все последующие действия будем делать в ней.
Настроим виртуальное Python окружение для изоляции приложения от других глобальных зависимостей системы:
Активируем виртуальное окружение:
Последующая установка новых Python пакетов будет производиться в данном виртуальной окружении.
Авторизация клиента
В качестве библиотеки для взаимодействия с Telegram API будем использовать Telethon:
Для загрузки значений переменных окружения установим пакет python-dotenv:
Создадим файл config.py, где будем хранить и загружать переменные окружения в память приложения:
Скрипт авторизации, целью которого является получение идентификатора сессии, выглядит следующим образом:
Запустим его:
После ввода номера телефона и кода, пришедшего в приложение Telegram, получаем заветный идентификатор сессии, который добавим в файл .env.
Теперь, используя вновь полученный идентификатор сессии, авторизуемся для дальнейшего взаимодействия с Telegram API:
Получение обновлений от Telegram
Присоединившись к каналу, серверы Telegram начинают слать нам соответствующие обновления (новые сообщения, отредактированные сообщения и др.). Остается лишь перенаправить их в наш канал.
Определим идентификаторы канала-источника (SOURCE_CHANNEL) и канала-приемника (TARGET_CHANNEL):
Получение новых сообщений
Следуя примеру из документации, добавим обработчик новых сообщений:
Для каждого нового сообщения в функцию handler_new_message приходит объект типа NewMessage, содержащий поле message с необходимой нам информацией. Т.к. нам необходимы обновления только от определенных каналов, при определении обработчика передадим ему параметр chats, содержащий идентификатор либо список идентификаторов каналов.
Редактирование сообщений
В случае переотправки пришедших сообщений (использование метода client.send_message), при отредактированном сообщении в канале-источнике, можем отредактировать его и в канале-приемнике.
Для реализации редактирования необходимо знать какое именно сообщение редактировать, поэтому при добавлении нового сообщения, нужно сохранить идентификаторы исходного и копии сообщения. В дальнейшем по идентификатору сообщения из источника сможем определить идентификатор сообщения в нашем канале.
Установим базу данных Postgres, если этого не было сделано ранее. Сама установка не должна вызвать сложностей:
Далее первоначальное взаимодействие с базой будем производить с помощью утилиты psql:
Для хранения соответствий идентификаторов определим простейшую таблицу BINDING_ID:
Создадим таблицу BINDING_ID в консоли утилиты psql командой:
Взаимодействие с базой данных будем производить с помощью пакета psycopg2:
В файл .env добавим строку подключения к базе данных вида:
Для работы с базой данных определим следующие базовые функции: добавление соответствия идентификаторов сообщений (insert) и поиск идентификатора сообщения-клона по идентификатору оригинального сообщения (find_by_id):
В функцию обработчика новых сообщений добавим сохранение идентификаторов сообщений в базе данных:
Тогда редактирование сообщений будет выглядеть следующим образом:
Ознакомиться с текущим вариантом проекта можно по ссылке ниже:
Для случая зеркалирования «один к одному» у нас все готово и можно переходить к развертыванию.
Зеркалирование «много к одному»
Когда каналов-источников и каналов, в которые необходимо пересылать сообщения, больше чем один, необходимо задать их взаимное соответствие.
Задавать карту соответствий прямо в коде — не вариант, поэтому для значения новой переменной окружения (CHANNELS_MAPPING) введем специальный формат записи:
Теперь вместо ранее заданных переменных SOURCE_CHANNEL и TARGET_CHANNEL имеем CHANNELS_MAPPING:
Для дальнейшей работы из строкового значения CHANNELS_MAPPING нужно получить представление в виде словаря (dict). Сделать это можно, разобрав строку по составляющим ее элементам с помощью регулярных выражений (модуль re) либо с помощью парсера грамматик (модуль pyparsing). В данном случае воспользуемся вторым вариантом.
Установим модуль pyparsing:
Так как входная строка для разбора состоит из отдельных элементов, для каждого такого элемента определим свою грамматику:
Основной элемент — идентификатор канала, состоящий из префикса -100 и последующих цифр:
Список каналов:
Полное выражение:
Подробнее о модуле pyparsing можно узнать в документации.
После составления грамматики, формируем результат разбора строки в виде словаря, где ключи — исходные каналы, значения — список целевых каналов (функция parse_string):
Инициализация переменной CHANNELS_MAPPING:
Расширим таблицу BINDING_ID двумя столбцами: идентификатор канала-зеркала (mirror_channel_id) и идентификатор канала-источника (channel_id):
И обновим функцию поиска сообщений-клонов в базе данных (find_by_id):
Тогда добавление новых сообщений будет выглядеть следующим образом:
Аналогично редактирование:
На вариант с множественным зеркалированием можно посмотреть по ссылке ниже:
Развертывание
Для развертывания приложения Heroku необходимо сделать следующее:
Отличная работа! А вы не думали добавить фильтрацию измененных/удаленных сообщений?
Пример: добавляем новые сообщения в базу, и мониторим их. Если такое сообщение будет изменено/удалено - только тогда отправляем в "зеркальный" канал
Heroku был хорош тем, что там можно было бесплатно захостить небольшое приложение и поднять БД, но хорошие времена прошли. Если есть желание и время самому покопаться, то рекомендую поднять Dokku в виртуальном облаке, например, на самом дешевом тарифе от timeweb за ~180₽. Или поискать зарубежные PaaS, где есть возможность запустить без оплаты.
Полезная штука. Тут чувак сделал наподобие, только управление редиректами сделано через бота https://github.com/rumble-key/feed-bot-telegram
классный гайд, можешь написать в личку - есть подобная задача, вдруг возможно сотрудничество с тобой!
Спасибо! Настроил себе все)
Подскажите, а почему позникает ошибка "Cant adapt type message"?
Отличная работа! А вы не думали добавить фильтрацию измененных/удаленных сообщений?
Пример: добавляем новые сообщения в базу, и мониторим их. Если такое сообщение будет изменено/удалено - только тогда отправляем в "зеркальный" канал
По ссылке в конце статьи находится обновленный проект, где можно добиться такого поведения изменив реплицирующие методы в EventProcessor'e ( https://github.com/khoben/telemirror/blob/a555136d3844381016916794ff8c78b9879eebf6/telemirror/mirroring.py#L16 ): на событие NewMessage только пишем в БД без отправки, на остальные события по наличию в БД отправляем сообщение.
Комментарий удален модератором
Heroku из России уже не доступен (не оплатить и т.д.). Можно все то-же самое сделать на Amvera Cloud, это отечественный аналог.
Heroku был хорош тем, что там можно было бесплатно захостить небольшое приложение и поднять БД, но хорошие времена прошли. Если есть желание и время самому покопаться, то рекомендую поднять Dokku в виртуальном облаке, например, на самом дешевом тарифе от timeweb за ~180₽. Или поискать зарубежные PaaS, где есть возможность запустить без оплаты.