Access hash telegram что это

Регистрируем в Telegram новое приложение
Для подключения к Telegram API необходимы api_id и api_hash . Эти параметры выдаются при регистрации приложения в инструментах разработчика (при отсутствии доступа используйте VPN). Для авторизации указываем номер телефона, к которому привязан аккаунт Telegram.

Вводим пришедший в Telegram численно-буквенный код и попадаем на страницу регистрации нового приложения. Заполняем форму, достаточно первых двух граф:

В результате попадаем на страницу конфигурации приложения. Находим оба параметра, а также доступные MTProto-сервера и открытые (публичные) ключи.
Избегая проблем с безопасностью, сохраняем учетные данные в отдельном файле config.ini следующей структуры:
[Telegram] api_id = Telegram-API-ID api_hash = Telegram-API-Hash username = Your-Telegram-Username
Поле username далее будет использоваться лишь для автоматического сохранения сессии под именем username.session . Одному клиенту соответствует одна сессия, учтите это в случае запуска нескольких клиентов.
Создаем клиент Telegram
Начнем с импорта библиотек.
import configparser import json from telethon.sync import TelegramClient from telethon import connection # для корректного переноса времени сообщений в json from datetime import date, datetime # классы для работы с каналами from telethon.tl.functions.channels import GetParticipantsRequest from telethon.tl.types import ChannelParticipantsSearch # класс для работы с сообщениями from telethon.tl.functions.messages import GetHistoryRequest
Встроенные модули configparser и json применяем соответственно для чтения параметров и вывода данных. Из библиотеки Telethon импортируем класс клиента Telegram и класс исключений. Внутренний модуль connection необходим при использовании прокси-сервера. Остальные элементы модуля telethon.tl используются для запросов необходимых нам списков (участников канала/чата и их сообщений).
Теперь считаем учетные данные из config.ini :
# Считываем учетные данные config = configparser.ConfigParser() config.read("config.ini") # Присваиваем значения внутренним переменным api_id = config['Telegram']['api_id'] api_hash = config['Telegram']['api_hash'] username = config['Telegram']['username']
Создадим объект клиента Telegram API:
client = TelegramClient(username, api_id, api_hash)
При необходимости прописываем прокси. При использовании протокола MTProxy прокси задается в виде кортежа (сервер, порт, ключ) .
proxy = (proxy_server, proxy_port, proxy_key) client = TelegramClient(username, api_id, api_hash, connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, proxy=proxy)
client.start()
При первом запуске платформа запросит номер телефона, и вслед – код подтверждения. Так же, как если бы вы входили в учетную запись в приложении или браузере.
Для сбора, обработки и сохранения информации мы создадим две функции:
- dump_all_participants(сhannel) заберет данные о пользователях администрируемого нами сообщества channel ;
- dump_all_messages(сhannel) соберет все сообщения. Для этой функции достаточно, чтобы у вас был доступ к сообществу (необязательно быть администратором).
Обе функции будут вызываться в теле функции main , в которой пользователь передаст ссылку на интересующий источник:
url = input("Введите ссылку на канал или чат: ") channel = await client.get_entity(url)
Касательно написания вызова функций стоит оговориться, что Telethon является асинхронной библиотекой. Поэтому в коде используются операторы async и await. В связи с этим функция main полностью будет выглядеть так:
async def main(): url = input("Введите ссылку на канал или чат: ") channel = await client.get_entity(url) await dump_all_participants(channel) await dump_all_messages(channel)
Заметим, что из-за асинхронности Telethon может некорректно работать в средах, использующих те же подходы (Anaconda, Spyder, Jupyter).
Рекомендуемым способом управления клиентом является менеджер контекстов with . Его мы запустим в конце скрипта после описания вложенных в main функций.
with client: client.loop.run_until_complete(main())
Собираем данные об участниках
Telegram не выводит все запрашиваемые данные за один раз, а выдает их в пакетном режиме, по 100 записей за каждый запрос.
async def dump_all_participants(channel): """Записывает json-файл с информацией о всех участниках канала/чата""" offset_user = 0 # номер участника, с которого начинается считывание limit_user = 100 # максимальное число записей, передаваемых за один раз all_participants = [] # список всех участников канала filter_user = ChannelParticipantsSearch('') while True: participants = await client(GetParticipantsRequest(channel, filter_user, offset_user, limit_user, hash=0)) if not participants.users: break all_participants.extend(participants.users) offset_user += len(participants.users)
Устанавливаем ограничение в 100, начинаем со смещения 0, создаем список всех участников канала all_participants . Внутри бесконечного цикла передаем запрос GetParticipantsRequest .
Проверяем, есть ли у объекта participants свойство users . Если нет, выходим из цикла. В обратном случае добавляем новых членов в список all_participants , а длину полученного списка добавляем к смещению offset_user . Следующий запрос забирает пользователей, начиная с этого смещения. Цикл продолжается до тех пор, пока не соберет всех фолловеров канала.
Самый простой способ сохранить собранные данные в структурированном виде – воспользоваться форматом JSON. Базы данных, такие как MySQL, MongoDB и т. д., стоит рассматривать лишь для очень популярных каналов и большого количества сохраняемой информации. Либо если вы планируете такое расширение в будущем.
В JSON-файле можно хранить и всю информацию о каждом пользователе, но обычно достаточно лишь нескольких параметров. Покажем на примере, как ограничиться набором определенных данных:
all_users_details = [] # список словарей с интересующими параметрами участников канала for participant in all_participants: all_users_details.append() with open('channel_users.json', 'w', encoding='utf8') as outfile: json.dump(all_users_details, outfile, ensure_ascii=False)
Итак, для каждого пользователя создается свой словарь данных и добавляется в общий список all_user_details , который записывается в JSON-файл.
Собираем сообщения
Ситуация со сбором сообщений идентична сбору сведений о пользователях. Отличия сводятся к трем пунктам:
- Вместо клиентского запроса GetParticipantsRequest необходимо отправить GetHistoryRequest со своим набором параметров. Так же, как и в случае со списком участников запрос ограничен сотней записей за один раз.
- Для списка сообщений важна их последовательность. Чтобы получать последние сообщения, нужно правильно задать смещение в GetHistoryRequest (с конца).
- Чтобы корректно сохранить данные о времени публикации сообщений в JSON-файле, нужно преобразовать формат времени.
import configparser import json from telethon.sync import TelegramClient from telethon import connection # для корректного переноса времени сообщений в json from datetime import date, datetime # классы для работы с каналами from telethon.tl.functions.channels import GetParticipantsRequest from telethon.tl.types import ChannelParticipantsSearch # класс для работы с сообщениями from telethon.tl.functions.messages import GetHistoryRequest # Считываем учетные данные config = configparser.ConfigParser() config.read("config.ini") # Присваиваем значения внутренним переменным api_id = config['Telegram']['api_id'] api_hash = config['Telegram']['api_hash'] username = config['Telegram']['username'] proxy = (proxy_server, proxy_port, proxy_key) client = TelegramClient(username, api_id, api_hash, connection=connection.ConnectionTcpMTProxyRandomizedIntermediate, proxy=proxy) client.start() async def dump_all_participants(channel): """Записывает json-файл с информацией о всех участниках канала/чата""" offset_user = 0 # номер участника, с которого начинается считывание limit_user = 100 # максимальное число записей, передаваемых за один раз all_participants = [] # список всех участников канала filter_user = ChannelParticipantsSearch('') while True: participants = await client(GetParticipantsRequest(channel, filter_user, offset_user, limit_user, hash=0)) if not participants.users: break all_participants.extend(participants.users) offset_user += len(participants.users) all_users_details = [] # список словарей с интересующими параметрами участников канала for participant in all_participants: all_users_details.append() with open('channel_users.json', 'w', encoding='utf8') as outfile: json.dump(all_users_details, outfile, ensure_ascii=False) async def dump_all_messages(channel): """Записывает json-файл с информацией о всех сообщениях канала/чата""" offset_msg = 0 # номер записи, с которой начинается считывание limit_msg = 100 # максимальное число записей, передаваемых за один раз all_messages = [] # список всех сообщений total_messages = 0 total_count_limit = 0 # поменяйте это значение, если вам нужны не все сообщения class DateTimeEncoder(json.JSONEncoder): '''Класс для сериализации записи дат в JSON''' def default(self, o): if isinstance(o, datetime): return o.isoformat() if isinstance(o, bytes): return list(o) return json.JSONEncoder.default(self, o) while True: history = await client(GetHistoryRequest( peer=channel, offset_id=offset_msg, offset_date=None, add_offset=0, limit=limit_msg, max_id=0, min_id=0, hash=0)) if not history.messages: break messages = history.messages for message in messages: all_messages.append(message.to_dict()) offset_msg = messages[len(messages) - 1].id total_messages = len(all_messages) if total_count_limit != 0 and total_messages >= total_count_limit: break with open('channel_messages.json', 'w', encoding='utf8') as outfile: json.dump(all_messages, outfile, ensure_ascii=False, cls=DateTimeEncoder) async def main(): url = input("Введите ссылку на канал или чат: ") channel = await client.get_entity(url) await dump_all_participants(channel) await dump_all_messages(channel) with client: client.loop.run_until_complete(main())
Если для анализа сообщений потребуются не все записи, задайте их число в переменной total_count_limit . Если нужна только сборка сообщений канала, достаточно закомментировать вызов await dump_all_participants(channel) .
Таким образом, с помощью Python и Telethon мы написали скрипт, собирающий и сохраняющий данные и реплики участников сообществ Telegram.
Авторизация пользователей через Telegram

Недавно Telegram добавил поддержку виджета для авторизации пользователей на сайте. Мы решили поэкспериментировать с ним и составить простую инструкцию, как настроить такую авторизацию самостоятельно.

В качестве примера будем использовать код на PHP, однако, данные шаги актуальны и для других языков программирования.
Настройка бота
Для использования виджета вам понадобится Telegram-бот.
Перейдите в чат с системным пользователем @botfather. Если у вас ещё нет ни одного бота, создайте его командой /newbot. Посмотреть список своих пользователей вы можете с помощью команды /mybots.

Скопируйте токен бота, через которого вы хотите производить авторизацию пользователей.
Here is the token for bot codex_cloud @codex_cloud_bot: 558<. >728:AWBEwgUg<. >HBKuiINt
Название и аватарка выбранного вами бота будут показаны пользователю во всплывающем окне. А вы получите возможность отправлять пользователю личные сообщения через этого бота.

Для каждого бота нужно привязать конкретный адрес сайта, на котором пользователи могут авторизоваться. В диалоге с @botfather введите команду /setdomain и напишите адрес сайта в виде http://ifmo.su.
Настройка виджета
На сайте можно получить код виджета и выбрать его внешний вид. К сожалению, возможностей для его произвольного конфигурирования на данный момент нет т.к. виджет встраивается на сайт посредством iframe.

Встраивание на сайт
Создайте файл index.php со следующим содержанием
После того, как пользователь нажмёт на кнопку, Telegram готов отправить вам данные любым из двух способов:
- Отправить пользователя на ваш сайт путём редиректа, передав информацию о нём в GET параметрах.
- Вызвать JavaScript функцию, передав в неё информацию о пользователе в качестве аргументов.
На данный момент поддерживаются следующие данные о пользователе:
- id – уникальный идентификатор пользователя в Telegram
- first_name, last_name – фамилия и имя из профиля пользователя
- username – уникальное имя из профиля
- photo_url – ссылка на аватарку пользователя в виде https://t.me/i/. /user.jpg
- auth_date – дата авторазации
- hash – HMAC-подпись ответа на основе секретного токена бота
Получение данных через JavaScript callback
Выберите в конструкторе виджета опцию Authorization Type: Callback. Сгенерированный в результате код виджета содержит JavaScript функцию, которая будет вызвана после успешной авторизации.
function onTelegramAuth(user) < alert('Logged in as ' + user.first_name + ' ' + user.last_name + ' (' + user.id + (user.username ? ', @' + user.username : '') + ')'); >
Эту функцию нужно передать в аттрибуте data-onauth тега
Вы можете произвольным образом реализовать функцию onTelegramAuth. Например, послать AJAX запрос на сервер с полученными аргументами.
Получение данных через Redirect
Выберите в конструкторе виджета опцию Authorization Type: Redirect to URL и введите URL, на который вы хотите получить запрос с данными пользователя. Например, введите адрес http://example.com/auth/telegram.
На странице обработки можно положить скрипт index.php следующего содержания:
Проверка данных пользователя
Чтобы удостовериться в правильности полученных данных, нужно проверить hash. Разработчики Telegram приводят пример кода проверки, добавим эту функцию в код из файла index.php
function checkTelegramAuthorization($auth_data) < $check_hash = $auth_data['hash']; unset($auth_data['hash']); $data_check_arr = []; foreach ($auth_data as $key =>$value) < $data_check_arr[] = $key . '=' . $value; >sort($data_check_arr); $data_check_string = implode(«\n», $data_check_arr); $secret_key = hash(‘sha256’, BOT_TOKEN, true); $hash = hash_hmac(‘sha256’, $data_check_string, $secret_key); if (strcmp($hash, $check_hash) !== 0) < throw new Exception('Data is NOT from Telegram'); >if ((time() — $auth_data[‘auth_date’]) > 86400) < throw new Exception('Data is outdated'); >return $auth_data; >
Разберём механизм работы функции проверки. В качестве аргумента она получает массив с данными пользователя.
array(7) < ["id"]=>string(7) «1831337» [«first_name»]=> string(18) «Александр» [«last_name»]=> string(16) «Менщиков» [«username»]=> string(5) «n0str» [«photo_url»]=> string(36) «https://t.me/i/userpic/100/n0str.jpg» [«auth_date»]=> string(10) «1518168109» [«hash»]=> string(64) «abba<..>1345″ >
На первом шаге из массива извлекается значение по ключу hash и сохраняется в переменной.
На втором шаге массив преобразуется к виду key=value и сортируется в лексикографическом порядке. Полученные данные склеиваются в одну строку через разделитель “\n” (код символа – 0xA0).
Далее происходит проверка равенства HMAC-SHA-256 подписи этой строки и значения сохранённого hash. Дополнительно проверяется не устарела ли auth_date.
В случае успеха, функция возвращает исходный массив без параметра hash.
Авторизация пользователя на сайте
Добавим в файл код вызова функции проверки
if (isset($_GET[‘hash’])) < try < $auth_data = checkTelegramAuthorization($_GET); echo "Hello, " . $auth_data['username']; >catch (Exception $e) < die ($e->getMessage()); > >
Пользователь увидит сообщение с приветствием в случае успешной авторизации. Теперь вы можете сохранить информацию о нём в базу данных и привязать его ID к текущей сессии.
Пример кода из рабочего проекта
try < $profile = $tg->checkTelegramAuthorization($_GET); $id = $profile[‘id’]; $user = Model_User::findByAttribute(‘telegram_id’, $id); if ($user->is_empty()) < $user = new Model_User(); $user->telegram_id = $id; . $user->save() > else < . >>
Кастомизация кнопки
Сейчас из-за ограничений iframe нельзя изменить внешний вид кнопки. Однако, если возникла сильная необходимость, можно обойти это ограничения с помощью clickjacking.
Внимание! Это решение не рекомендуется к использованию. Clickjacking вводит пользователей в заблуждение, а поисковые системы могут понизить ваш сайт в выдаче в качестве наказания.
Идея состоит в том, чтобы разместить iframe с кнопкой поверх ссылки, оформленной по вашему вкусу. Если такой iframe сделать прозрачным, то пользователь кликнет внутрь него, пытаясь нажать на ссылку.
Итоги
Telegram выпустила полезный инструмент, который позволяет авторизовать пользователей на своём сайте и привязать их профиль к Telegram-аккаунту. К сожалению, пока не поддерживается свободная кастомизация их виджета, а также нет удобного API для аутентификации. Вероятно, в ближайшее время они добавят такие возможности.
Ссылки для подробного изучения
- Официальная документация – https://core.telegram.org/widgets/login
- Анонс виджета – https://telegram.org/blog/login
- Документация по боту Telegram – https://core.telegram.org/bots#creating-a-new-bot
- Примеры кода проверки авторизации – https://gist.github.com/anonymous/6516521b1fb3b464534fbc30ea3573c2
If you like this article, share a link with your friends
Read more
We talk about interesting technologies and share our experience of using them.
Пользователи и личные сообщения
Бот может написать пользователю в личный чат только с его разрешения. Если диалог уже начат, бот может отправлять сообщения в любой момент.
Бот не может писать другим ботам.
1. Когда пользователь запускает бота
Личная переписка пользователя с ботом начинается, когда пользователь открывает бота и нажимает на кнопку «Запустить».
Открыть бота впервые пользователь может, например, по ссылке или через поиск в приложении. В этот момент пользователь увидит текст «Что может этот бот» и кнопку «Запустить». В зависимости от платформы и языка пользователя кнопка эта также может называться «Начать» или «Start».
Нажатие на эту кнопку отправляет команду /start . Боту следует отвечать на неё приветствием или инструкцией по использованию.
Когда у пользователя появится переписка с ботом, он увидит бота в списке недавних чатов.
Команда /start не обязательно означает, что это первое сообщение от пользователя. Убедитесь, что ваш бот не ломается, если пользователь отправил /start вручную уже после запуска.
Более того, первое сообщение от пользователя может не содержать команду /start . Через Telegram API пользователь может начать диалог с любого сообщения. Вряд ли у вашего бота будут такие ненормальные пользователи, но лучше проверьте, что такое действие не кладёт вашего бота.
Чтобы сообщение /start должно содержать дополнительную информацию, используйте диплинки.
2. В других случаях
Бот также может написать пользователю, если:
- пользователь оставил заявку на вступление в группу;
- пользователь авторизовался через бота на сайте с Telegram Login Widget.
Перед диалогом пользователь видит, по какой из этих причин бот может писать пользователю.
Остановка диалога
Пользователь может вновь остановить переписку, заблокировав бота. Это значит, что бот снова не сможет отправлять пользователю сообщения, пока тот его не запустит.
В последних версиях большинства официальных приложений Телеграма, блокируя бота, пользователь также очищает историю переписки. Поэтому, если после блокировки пользователь запустит бота снова, скорее всего, пользователь уже не увидит старые сообщения.
Как проверить, может ли бот писать пользователю
Попробуйте показать в чате с пользователем статус «Бот печатает. ». Если сервера Телеграма вернули ошибку, вы не можете писать пользователю.
Это значит, что пользователь заблокировал бота или бот никогда не мог писать этому пользователю.
Профиль пользователя
Если вы будете хранить пользователей (в базе данных, например) — учтите, что у пользователя обязательно должно быть имя, но необязательно фамилия и юзернейм. Кроме того, все эти данные пользователи могут в любой момент поменять; поэтому различать пользователей следует не по юзернейму, а по их id.
Языки пользователей
Боты видят язык, установленный у пользователя в приложении Телеграма. Таким образом ваш бот может переписываться с пользователем на его языке.
В апдейтах не всегда отображается язык пользователя. Поэтому, если ваш бот подстраивается под языки пользователей, для пользователя с неустановленным языком документация Телеграма рекомендует использовать последний язык, который был известен для этого пользователя, или английский, если такого языка нет.
«Знакомые» пользователи
Или пользователи которых «видел» пользователь/бот
Чтобы посылать к API запросы, касающиеся пользователя, недостаточно знать только id этого пользователя: нужен еще и access hash. API отдаёт access hash, когда бот «встречает» пользователя, то есть когда API даёт информацию о профиле пользователя. Это происходит, например, если пользователь отправил сообщение боту или бот получил нужного пользователя по юзернейму.
Bot API и большинство библиотек под Telegram API кэшируют access hash, так что вам не нужно сохранять и использовать его самому.
Делаем OAuth авторизацию Telegram на своём сайте
Раньше тоже можно было авторизовать пользователя через телеграм. Но для этого и пользователю и разработчику нужно было сделать много лишних телодвижений.
А теперь всё просто как в ВК или Facebook.
Что нам нужно?
На нужен вебсайт. На localhost авторизацию проверить не получится, потому что к боту привязывается доменное имя.
Создаём бота
Создать бота всё-таки придётся. Авторизовываться будут с помощью него. Разработчики Телеграм говорят, что бот сможет сам инициировать диалог с авторизованными таким способом пользователями. Я этого пока не проверял. Поверим на слово.
Подробно писать как создать бота я не буду, потому что про это написана и перенаписано миллион статей. Вот ссылка на мануал от Telegram https://core.telegram.org/bots#3-how-do-i-create-a-bot .
После создания бота нам дадут ключ api, который пригодится нам в дальнейшем.
Привязываем к боту домен
Нужно отправить боту @BotFather команду /setdomain , он сначала попросит выбрать бота, а потом нужно будет ввести адрес сайта.
Виджет авторизации
На странице https://core.telegram.org/widgets/login находим конфигурацию виджета
Вводим username бота, который сами придумали в процессе создания бота.
Далее выбирает размер кнопки. Три варианта. При перещёлкивании кнопка внизу будет автоматически менять размер.
Затем нужно выбрать как мы будем обрабатывать данные полученные от телеграм. Тут два варианта. Либо мы делает под это отдельную страницу и пользователя перенаправить туда, мы обработаем данные и можем перенаправить обратно или, например в закрытую часть сайта. Либо же мы делаем всё это через callback без перезагрузки страницы. Второй вариант мне кажется правильнее и современнее. Его и выбираю.
Далее там самая галочка, которая видимо и отвечает за отсылку сообщений от имени бота
Ну дальше поле с получившимся кодом.
Тут два тега скрипт. В принципе можно всё это вставить в нужное место на сайте, но я вставил только верхний, который рисует кнопку. Из нижнего я взял callback функцию и вставил в общий js-файл для сайта.
Сейчас эта функция просто выводит стандартный alert в браузере с сообщением о том, что пользователь залогинен и некоторые его данные. Позже напишем сюда запрос к серверу.
На сайте
На клиенте
Как уже написал чуть выше, скрипт, отрисовывающий кнопку входа в телеграм, я вставил в нужное место в разметке. В файл, где у меня прописаны основные js функции, я добавил ту самую callback-функцию. У меня на сайте установлен jquery, поэтому не стал выделываться и отправил запрос через него.
На сервере
Сайт работает на nodejs на фреймворке koa, поэтому пример буду приводить на нём(в конце дам ссылку на php)
Это контроллер login
//подключаем необходимые модули
const Router = require(‘koa-router’)
const router = new Router()
const jwt = require(‘jsonwebtoken’)
const config = require(‘../config/config’)
const mongo = require(‘../config/mongo’)
const crypto = require(‘crypto’);
const < strcmp >= require(‘../lib/utils’)
const ObjectID = require(‘../config/mongo’).ObjectID
// тут роут
exports.init = function (app)
router.post(‘/login’, login)
app.use(router.routes())
>
// собственно функция логина
async function login(ctx,next)
// сюда методом post приходят данные в виде json
const authData = ctx.request.fields
//с помощью hash мы проверим целостность данных, то есть вообще с телеграма ли нам это пришло или кто-то нехороший копается
const checkHash = authData.hash
delete authData[‘hash’]
/* по инструкции телеграм мы должны взять все данные, кроме hash, которые пришла нам от телеграм и собрать из в одну строку в формате key=value, разделяя символом переноса строки \n */
let dataCheck = []
for (let key in authData)
dataCheck.push(key + ‘=’ + authData[key])
>
dataCheck.sort();
dataCheck.join(“\n”)
// Делаем из неё sha256
const secretKey = crypto.createHash(‘sha256’)
.update(config.oauth.telegram.botToken)
/* и проверяем если не ок, то шлём юзера куда подальше, если ок, то добавляем или обновляем пользователя в базе данных */
const hash = crypto.createHmac(‘sha256’, dataCheck.toString(), secretKey);
if (strcmp(hash, checkHash) === -1)
ctx.status = 401
ctx.body = ‘Data is NOT from Telegram’
>
if ( +(new Date()) — authData.auth_date > 86400)
ctx.status = 401
ctx.body = ‘Data is outdated’
>
const user = await mongo.users.findOne()
if(!user)
await mongo.users.insertOne(authData)
>else
await mongo.users.updateOne(,>)
>
/*Здесь я использовал jwt, чтобы сделать токен, в принципе можно написать это самому, но суть статьи не в этом*/
const token = await jwt.sign(authData, config.app.secret)
/* Ставлю куку с этим токеном */
/*Теперь я добавлю перед всеми роутами middlware. Я использую koa, там принять пользователя хранить в ctx.state.user*/
app.use(async (ctx,next) =>
const token = ctx.cookies.get(‘tgUser’)
if(token)
try
ctx.state.user = await jwt.verify(token, config.app.secret)
>catch (error)
ctx.cookies.set(‘tgUser’, ‘’);
>
>
await next(ctx)
>)
После этого остаётся где нужно проверять переменную ctx.state.user.
Есть пример намного проще и на php от самих Телеграм. Вероятно, самые внимательные его уже нашли https://gist.github.com/anonymous/6516521b1fb3b464534fbc30ea3573c2
Этот пример чисто демонстративный, для реального сайта его нужно будет допилить.