Содержание
06 ноября 2019 Рассказываем, как собрать данные участников и сообщения из чатов/каналов Telegram с помощью Python и библиотеки Telethon. 26
Вводим пришедший в Telegram численно-буквенный код и попадаем на страницу регистрации нового приложения. Заполняем форму, достаточно первых двух граф:
</p></picture>
Захотелось как-то мне, чтобы сообщения одного из чатов телеграма сохранялись у меня на диске (не запуская обычного клиента). Не буду раскрывать своих побудительных мотивов, но возможность эта показалась мне нужной и полезной.
Для этого в телеграме есть боты. На Хабре есть несколько статей, посвященных ботам, например: “Чат-помощник на сайт”.
Бот позволяет читать и посылать сообщения, для регистрации бота не нужен телефон и количество ботов может быть любым. Но название бота включает в себя слово “bot”, что может вызвать у хозяина чата ненужные вопросы.
Но, как говорится, правильно поставленный вопрос — половина ответа.
Оказывается кроме “API telegram bot” существует еще и “API telegram client”, т.е. API для создания собственных клиентов.
Клиент также может посылать и читать сообщения, но только от зарегестрированного (привязанного к телефону) пользователя, что мне как раз подходит (я уже зарегестрирован в чате).
На сайте телеграма есть список API для разных платформ: https://telegram.org/apps#source-code
Однако, самой простой в использовании оказалась библиотека на python: Pure Python 3 MTProto API Telegram client library под названием “telethon”
pip3 install telethon
Подводные камни, встреченные мною при инсталляции:
- не инсталлирован pip3 (инсталлятор для питона).
sudo apt-get -y install python3-pip
- Библиотека работает только на питоне версии >3.5. Так что, возможно, придется его обновить.
Все установилось. Листаем readme.txt дальше.
Следущим пунктом идет создание клиента телеграма… Как, уже? Ну да, все просто. Правда, сперва нужно зарегистритровать себя как создателя клиента.
Заходим на сайт телеграма: https://my.telegram.org Вводим телефон и ждем код подтверждения на родном клиенте телеграма. Он довольно длинный (12 символов) и неудобный для ввода.
Заходим в пункт “API”. Ищем “Telegram API” и заходим в “Creating an application” (https://my.telegram.org/apps).
Заполняем поля App title и Short name, нажимаем «Create application» и запоминаем две переменные: api_id и api_hash.
Пришла пора делать клиента.
from telethon import TelegramClient, sync # Вставляем api_id и api_hash api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' client = TelegramClient('session_name', api_id, api_hash) client.start()
session_name — можно вставить любое имя. Вас попросят ввести телефон и пришлют код подтверждения. После этого клиент будет работать без запроса телефона (до тех пор, пока не поменяете session_name). Рядом с вашей программой появится файл session_name.session
Если ошибок нет, клиент готов. Только вот, ничего не выводит. Попробуем получить полезную инфорфмацию.
Узнаем немного о себе:
print(client.get_me().stringify())
результат выдается в виде:
User( photo=None, last_name='Pupkin', first_name='Vasya', id=123456789, phone='79041234567', .... - что-то еще... )
Можем послать сообщение от себя:
client.send_message('username', 'Hello! Talking to you from Telethon')
Можно и картинку
client.send_file('username', '/home/myself/Pictures/holidays.jpg')
Как меня видят другие:
client.download_profile_photo('me')
Смотрим, на какие чаты мы подписаны:
print all chats name for dialog in client.iter_dialogs(): print(dialog.title)
читаем все сообщения чата “chat_name” (осторожно, сообщений может быть очень много)
messages = client.get_entity('chat_name') print(messages)
просмотр всех пользователей чата
participants = client.get_participants('chat_name') print(participants)
Нам нужна программка, следящая за новыми сообщениями в определенном канале.
Чтобы клиент не заканчивал работу, после client.start() вставляем строку:
client.run_until_disconnected()
Эта конструкция (вставляется перед client.start()) выводит только новые сообщения:
@client.on(events.NewMessage(chats=('chat_name'))) async def normal_handler(event): # print(event.message) print(event.message.to_dict()['message'])
Давайте разберемся.
@client.on(events.NewMessage(chats=('chat_name')))
создает событие, срабатывающее при появлении нового сообщения
print(event.message)
выводит сообщение в таком виде:
Message(edit_date=None, views=None, reply_markup=None, fwd_from=None, id=187, entities=[], post=False, mentioned=False, via_bot_id=None, media_unread=False, out=True, media=None, date=datetime.datetime(2018, 10, 1, 9, 26, 21, tzinfo=datetime.timezone.utc), to_id=PeerChannel(channel_id=123456789), reply_to_msg_id=None, from_id=123456789, silent=False, grouped_id=None, post_author=None, message='hello telegram')
Из всего этого нам нужно поле: “message=’hello telegram'”:
print(event.message.to_dict()['message'])
Сообщение получили, но от кого оно, непонятно, т.к. в сообщение только ID пользователя. Чтобы сопоставить ID и имя пользователя, скачиваем всех пользователей чата и помещаем их в словарь (хэш) в виде d[id]=”first_name last_name”
participants = client.get_participants(group) users={} for partic in client.iter_participants(group): lastname="" if partic.last_name: lastname=partic.last_name users[partic.id]=partic.first_name+" "+lastname
Теперь мы можем узнать, кто послал сообщение:
s_user_id=event.message.to_dict()['from_id'] user_id=int(s_user_id) user=d.get(user_id)
В принципе, можно получить имя пользователя из телеграма напрямую, но если пользователей немного, проще со словарем.
Вытаскиваем из сообщения дату отправки:
mess_date=event.message.to_dict()['date']
f=open('messages_from_chat', 'a')
И запишем сообщение:
f.write(mess_date.strftime("%d-%m-%Y %H:%M")+"n") f.write(user+"n") f.write(user_mess+"nn") f.flush()
Вот и все! Все, что мне было нужно, программка делает. Утилитка, конечно, сыровата, но свою задачу выполняет.
Python оказался не таким уж и сложным <strike>как его малюют</strike>, тем более описание и разных библиотек в интернете полным-полно. Написать еще пару утилиток и привыкнув к нему, можно использовать его как скриптовый язык вместо bash.
Весь текст утилиты:
from telethon import TelegramClient, sync, events api_id = 12345 api_hash = '0123456789abcdef0123456789abcdef' client = TelegramClient('session_name', api_id, api_hash) @client.on(events.NewMessage(chats=('chat_name'))) async def normal_handler(event): # print(event.message) user_mess=event.message.to_dict()['message'] s_user_id=event.message.to_dict()['from_id'] user_id=int(s_user_id) user=d.get(user_id) mess_date=event.message.to_dict()['date'] f.write(mess_date.strftime("%d-%m-%Y %H:%M")+"n") f.write(user+"n") f.write(user_mess+"nn") f.flush() client.start() group='group_name' participants = client.get_participants(group) users={} for partic in client.iter_participants(group): lastname="" if partic.last_name: lastname=partic.last_name users[partic.id]=partic.first_name+" "+lastname f=open('messages_from_chat', 'a') client.run_until_disconnected() f.close()
Полное описание библиотеки.
Эта заметка не про Bot API, а про Core API Telegram, с помощью которого можно создавать полноценные клиенты для месседжера, и конечно же любой другой софт, например для сбора данных из Телеграма. Основная проблема заключается в том, что общение с серверами Telegram осуществляется по специальному протоколу разработанным внутри компании — MTProto. Именно благодаря этому протоколу данный месседжер и славится своей безопасностью и шифрование данных.
Вас мучают вопросы: как использовать Telegram Api на PHP? Как вызывать функции? Очень много примеров использования telegram api для бота, а как использовать обычное api telegram? Зарегистрировал приложение, получил api_id и api_hash, как получить все сообщения из телеграм-канала? https://core.telegram.org/method/messages.getHistory Как вызвать этот метод? Как реализовать авторизацию с помощью API Telegram? Тогда эта статья для вас! Естественно, разбирать нюансы протокола MTProto в данной заметке я не буду. Для работы с ним буду пользоваться PHP-библиотекой MadelineProto, доступной всем желающим на GitHub. Однако, нельзя просто так взять и воспользоваться библиотекой. Есть как минимум три нюанса, которые нужно решить.
Подготовка к установке MadelineProto
Во-первых, нужен установленный Python, будет достаточно версии 2.7.
Во-вторых, библиотека не помечена как стабильная, поэтому для подключения её через composer к существующему проекту нужно немного отредактировать composer.json:
"minimum-stability": "dev",
Для того, чтобы composer не ругался на отсутствие стабильных версий зависимостей. Без указания этой директивы во время установки библиотеки будет получена примерно такая ошибка:
Your requirements could not be resolved to an installable set of packages. Problem 1 - Installation request for danog/madelineproto ^2.0 -> satisfiable by danog/madelineproto[2.0]. - danog/madelineproto 2.0 requires danog/primemodule dev-master -> satisfiable by danog/primemodule[dev-master] but these conflict with your requirements or minimum-stability. Installation failed, reverting ./composer.json to its original content.
Затем нужно указать git-репозиторий библиотеки:
"repositories": [ { "type": "git", "url": "https://github.com/danog/phpseclib" } ],
и только затем можно устанавливать саму либу:
composer require danog/madelineproto
Если во время установки зависимостей появится ошибка на подобии такой:
github Failed to clone via https, ssh protocols, aborting.
error:1407742E:SSL routines:SSL23_GET_SERVER_HELLO:tlsv1 alert protocol version
То вам нужно обновить версию git.
Регистрация приложения для Telegram API
Теперь нужно зарегистрировать приложение в разделе API development tools и получить App api_id и App api_hash.
Как правильно использовать MadelineProto с Laravel
В-третьих, на сегодняшний день (2017-02-10) мне не удалось запустить MadelineProto из коробки, т.к. начинали сыпаться ошибки типа:
DataCenter: Connecting to DC 2 (main server, ipv4, tcp_full)... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 DataCenter: Connection failed, retrying connection on port 443... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 DataCenter: Connection failed, retrying connection on port 80... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 DataCenter: Connection failed, retrying connection on port 88... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 DataCenter: Connection failed, retrying connection on port 443 without the proxy... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 DataCenter: Connection failed, retrying connection on port 80 without the proxy... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 DataCenter: Connection failed, retrying connection on port 88 without the proxy... Exception: stream_set_timeout() expects parameter 1 to be resource, null given in Socket.php:153 Exception: Undefined offset: 2 in MsgIdHandler.php:77 CallHandler: An error occurred while calling method help.getNearestDc: Undefined offset: 2 in MsgIdHandler on line 77. Recreating connection and retrying to call method... Exception: Undefined offset: 2 in MTProto.php:641 In MTProto.php line 641: Undefined offset: 2
На самом деле здесь нет ничего фатального, просто фреймворк Laravel по-умолчанию перехватывает все ошибки и при отсуствии должных обработчиков завершает скрипт даже при наличии не критичных ошибок. Возможно такое поведение присутствует и в других фреймворках. Можно изменить уровень ошибок, добавив в метод AppProvidersAppServiceProvider::boot() строку:
error_reporting(0);
Но тогда есть вероятность пропустить некритичные ошибки своего приложения.
Вторым способом устранения ошибок будет правка исходника /vendor/danog/madelineproto/src/danog/MadelineProto/Connection.php, а именно нужно закомментировать 3 строки в конструкторе в условии
case 'tcp_full':
// $this->sock->setOption(SOL_SOCKET, SO_RCVTIMEO, $timeout); // $this->sock->setOption(SOL_SOCKET, SO_SNDTIMEO, $timeout); // $this->sock->setBlocking(true);
В коммите 56c0d431768c04009ae9aa3151715b5e6399ec4d эти строки находятся на 105-107 строках файла. Источник проблемы был найден с помощью отладчика xDebug. Проблема заключалась в том, что методы $this->sock->setOption() и $this->sock->setBlocking() пытались работать с ещё не созданным объектом $this->sock->sock. Если у вас возникнут другие ошибки, то с помощью отладчика вы их легко обнаружите и исправите.
Также в библиотеку могут быть зашиты устаревшие или не актуальные IP-адреса серверов Телеграма. Их всегда можно посмотреть на странице API development tools и передать в ModelineProto через конструктор danogMadelineProtoAPI().
Список всех параметров которые можно изменить в этой библиотеке можно посмотреть в массиве $default_settings метода danogMadelineProtoMTProto::parse_settings().
В идеале нужно зарегистрировать для приложения новую чистую учётную запись, но для тестирования и отладки вполне сгодится любая уже имеющаяся учётка Телеграма.
Пример работы MadelineProto на Laravel
Как делать запросы к Telegram API на PHP?
Приведу простой пример кода на базе консольной команды для Laravel:
public function handle() { // Если файл с сессией уже существует, использовать его if(file_exists( env('TELEGRAM_SESSION_FILE') ) ) { $madeline = new API( env('TELEGRAM_SESSION_FILE') ); } else { // Иначе создать новую сессию $madeline = new API([ 'app_info' => [ 'api_id' => env('TELEGRAM_API_ID'), 'api_hash' => env('TELEGRAM_API_HASH'), ] ]); // Задать имя сессии $madeline->session = env('TELEGRAM_SESSION_FILE'); // Принудительно сохранить сессию $madeline->serialize(); // Начать авторизацию по номеру мобильного телефона $madeline->phone_login( env('TELEGRAM_PHONE') ); // Запросить код с помощью консоли $code = readline('Enter the code you received: '); $madeline->complete_phone_login($code); } $messages = $madeline->messages->getHistory(['peer' => '@ANY_CHANNEL_ID', 'offset_id' => 0, 'offset_date' => 0, 'add_offset' => 0, 'limit' => 10, 'max_id' => 0, 'min_id' => 0, 'hash' => 0, ]); foreach($messages['messages'] as $msg) { dump($msg); } }
Для тех, кто не умеет в Laravel, кратко поясню. Вызовы env() — это запросы значений из файла конфигурации, можно заменить их на константы или захардкодить. Собственно:
TELEGRAM_SESSION_FILE — любое значение, которое можно использовать в качестве имени файла.
TELEGRAM_API_ID и TELEGRAM_API_HASH — Данные из API development tools.
TELEGRAM_PHONE — мобильный номер существующий учётки, например, +7XXXXXXXXXX.
Теперь пояснения о происходящем в коде. Сессия — достаточно важный объект клиента, без него при каждом запуске скрипта авторизовываться и вводить код из сообщения, который Telegram высылает либо в смс либо через сам месседжер. Также на этапе авторизации происходит вся знаменитая шифровочная телеграм-магия. Процесс не быстрый, на моём тестовом стенде он мог затягиваться на минуты.
В результате работы скрипта в консоль будет выведено огромное количество отладочной информации, на которую можно не обращать внимания. При первом запуске сессии нужно будет авторизоваться в месседжере:
API: Running APIFactory... API: MadelineProto is ready! API: Serializing MadelineProto... Login: Sending code... Login: Code sent successfully! Once you receive the code you should use the complete_phone_login function. Enter the code you received: ... Login: Logging in as a normal user... MTProto: Trying to copy authorization from dc 2 to dc 1 MTProto: Trying to copy authorization from dc 2 to dc 3 MTProto: Trying to copy authorization from dc 2 to dc 4 ResponseHandler: Parsing updates received via the socket...
После чего можно полноценно использовать все возможности Telegram Core API, например, $messages = $madeline->messages->getHistory().
Данный метод возвращает сообщения из канала в обратном хронологическом порядке, т.е. начиная с самых свежих. Подробнее о параметрах этого метода можно узнать на страницах официальной документации MadelineProto. Заметьте, параметры MadelineProto могут отличаться от параметров официальной документации самого Telegram.
Используемые источники:
- https://proglib.io/p/pishem-prostoy-grabber-dlya-telegram-chatov-na-python-2019-11-06
- https://habr.com/ru/post/425151/
- https://evilinside.ru/kak-na-php-rabotat-s-telegram-core-api-mtproto/