TG.JoinProof
_ __ __ _ _ __ __ _
| | \ \ / / | | | \ \ / / | |
| |__ _ _ \ \_/ /__| | | _____ _\ \ /\ / /__| |__
| '_ \| | | | \ / _ \ | |/ _ \ \ /\ / /\ \/ \/ / _ \ '_ \
| |_) | |_| | | | __/ | | (_) \ V V / \ /\ / __/ |_) |
|_.__/ \__, | |_|\___|_|_|\___/ \_/\_/ \/ \/ \___|_.__/
__/ |
|___/ https://yellowweb.top
If you like this script, PLEASE DONATE!
TG.JoinProof связывает рекламный click_id с фактическим вступлением пользователя в Telegram-канал или чат. Система создаёт персональную инвайт-ссылку, принимает событие chat_member от Telegram и отправляет GET-постбэк в YellowTDS, AIO, Binom или другой трекер.
Проект работает на PHP и SQLite без Composer и внешних PHP-библиотек. Интерфейс администратора доступен на русском и английском языках.
- Telegram-каналы, обычные группы и супергруппы.
- Несколько ботов и несколько целей у одного бота.
- Персональный инвайт на одного участника со сроком действия 24 часа.
- Одна запись и одна конверсия для пары
target + click_id. - Повторное использование действующего инвайта и автоматическое обновление истёкшего.
- Отзыв персонального инвайта сразу после вступления.
- Fallback после состоявшегося вступления: публичная ссылка Telegram либо общий инвайт приватной цели.
- До трёх попыток доставки постбэка.
- Макросы click ID и данных пользователя Telegram.
- Статистика, журналы событий и RU/EN-интерфейс.
Панель с подключённым приватным чатом:
Английская версия того же экрана:
Форма добавления канала или чата:
-
Трекер подставляет свой click ID в URL прямого редиректа:
https://example.com/tg-postback/subscribe.php?target=-1003410571937&click_id={clickid} -
В
targetпередаётся числовой Telegram ID канала или чата, например-1003410571937.@usernameв публичной ссылке не используется. -
При первом запросе система создаёт через Telegram Bot API инвайт с
member_limit=1и сроком действия 24 часа, сохраняет его вместе сclick_idи перенаправляет пользователя в Telegram. -
Telegram присылает
chat_member, когда пользователь действительно вступает. Система сопоставляет бота, Telegram chat ID и использованный инвайт. -
Инвайт отмечается использованным и отзывается, после чего в трекер отправляется postback.
Вступлением считается новый статус участника member, а также restricted с is_member=true. Изменение ограничений уже состоящего в чате пользователя не создаёт новую конверсию.
Состояние пары target + click_id |
Результат запроса |
|---|---|
| Записи ещё нет | Создаётся персональный инвайт на одного участника сроком на 24 часа |
| Инвайт действует, вступления не было | Возвращается тот же персональный инвайт |
| Инвайт истёк, вступления не было | Создаётся новый текущий инвайт; старый остаётся в истории для задержанного Telegram update |
| Вступление уже зафиксировано | Новая конверсия не создаётся; пользователь направляется по fallback-ссылке |
Для публичной цели fallback — https://t.me/username. Для приватной цели при её добавлении автоматически создаётся отдельный общий инвайт без отслеживания. Один и тот же click_id можно использовать для разных target: это независимые пары.
Конверсия фиксируется только при реальном переходе из состояния «не участник» в состояние «участник». Если человек уже состоит в канале или чате, нового события вступления и новой конверсии не будет.
- PHP 8.1 или новее.
- Расширения PHP:
curl,json,pdo_sqlite. - Nginx, Apache или другой веб-сервер с PHP-FPM.
- Публично доступный HTTPS-адрес с действительным сертификатом для Telegram webhook.
- Telegram-бот с правами администратора в каждой подключённой цели.
git clone https://github.com/dvygolov/TG.JoinProof.git
cd TG.JoinProof
cp .env.example .envПроект использует чистую схему SQLite и не содержит миграций. Схема создаётся автоматически при первом обращении к панели, если по указанному DB_PATH нет таблиц. Непустая база не изменяется.
Минимальная конфигурация:
DB_PATH=database.sqlite
APP_URL=https://example.com
LOG_PATH=logs/
INVITE_TTL=86400
POSTBACK_MAX_ATTEMPTS=3
POSTBACK_TIMEOUT=5
ADMIN_PASSWORD=replace_with_a_long_random_passwordОсновные параметры:
| Переменная | Назначение |
|---|---|
DB_PATH |
Относительный от каталога проекта или абсолютный путь к SQLite |
APP_URL |
Внешний адрес приложения без завершающего / |
LOG_PATH |
Относительный или абсолютный каталог журналов |
INVITE_TTL |
Срок персонального инвайта в секундах; штатное значение 86400 |
POSTBACK_MAX_ATTEMPTS |
Максимальное число попыток; штатное значение 3 |
POSTBACK_TIMEOUT |
Таймаут одной попытки в секундах; штатное значение 5 |
ADMIN_PASSWORD |
Пароль входа в панель |
Для установки в подпапку укажите полный префикс:
APP_URL=https://example.com/tg-postbackИменно APP_URL используется при регистрации webhook и формировании публичной ссылки.
Для production рекомендуется хранить SQLite в отдельном непубличном каталоге. Укажите в .env:
DB_PATH=/var/lib/tg-joinproof/database.sqliteСоздайте каталог базы и каталог журналов до запуска установки:
sudo install -d -o www-data -g www-data -m 770 /var/lib/tg-joinproof
sudo chown -R www-data:www-data logs
sudo chmod 770 logsSQLite создаёт journal/WAL-файлы рядом с базой, поэтому PHP-процессу веб-сервера нужны права записи не только на файл, но и на родительский каталог. Саму .env оставьте доступной только владельцу и группе веб-сервера:
sudo chown root:www-data .env
sudo chmod 640 .envНе размещайте .env, SQLite и logs/ внутри публичного каталога. Веб-корнем должен быть только каталог public/.
Минимальный пример Nginx:
server {
listen 443 ssl;
server_name example.com;
root /var/www/tg-postback/public;
index index.php;
location / {
try_files $uri $uri/ =404;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
}Путь к сокету PHP-FPM зависит от установленной версии PHP. SSL-параметры и сертификат добавьте в соответствии с конфигурацией сервера.
Для Apache задайте DocumentRoot на /var/www/tg-postback/public и разрешите обработку PHP в этом каталоге. В проекте уже есть public/.htaccess.
Пример для адреса https://example.com/tg-postback/ в существующем Nginx-сервере:
location = /tg-postback {
return 301 /tg-postback/;
}
# Разместите этот блок раньше общего обработчика PHP сайта.
location ~ ^/tg-postback/(.+\.php)$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/tg-postback/public/$1;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location /tg-postback/ {
alias /var/www/tg-postback/public/;
index index.php;
}После изменения Nginx:
sudo nginx -t
sudo systemctl reload nginxДля Apache можно использовать:
Alias /tg-postback/ /var/www/tg-postback/public/
<Directory /var/www/tg-postback/public>
DirectoryIndex index.php
AllowOverride All
Require all granted
</Directory>Для проверки интерфейса без Telegram webhook:
php -S 127.0.0.1:8080 -t publicПанель откроется по адресу http://127.0.0.1:8080. Полный сценарий вступления требует публичного HTTPS URL, доступного серверам Telegram.
- Создайте бота через @BotFather и сохраните токен.
- Добавьте бота администратором нужного канала или чата.
- Разрешите боту приглашать пользователей — в Telegram это право называется
can_invite_users, «Приглашение пользователей» или “Invite users via link”. - Остальные административные права для работы системы не требуются.
Один Telegram-бот может иметь только один webhook. Не используйте подключённого бота одновременно в другом приложении, которое также вызывает setWebhook.
- Откройте панель по адресу из
APP_URLи войдите с паролем из.env. - В разделе «Боты» добавьте название и токен. Система проверит токен.
- В разделе «Чаты и каналы» добавьте цель:
- выберите бота;
- укажите числовой Telegram ID канала или чата (например
-1003410571937) либо@usernameпубличной цели; ссылку видаt.me/nameсистема тоже примет; - при необходимости задайте отображаемое название;
- вставьте URL постбэка трекера.
- При добавлении система проверит тип цели и права бота, сохранит канонический Telegram chat ID, подготовит fallback и зарегистрирует webhook.
- Нажмите кнопку копирования у созданной цели. Полученная ссылка содержит Telegram ID цели в
targetи учитывает подпапку изAPP_URL.
В интерфейсе обычная группа и супергруппа отображаются единым типом «Чат», канал — типом «Канал».
Единственный поддерживаемый публичный формат:
GET /subscribe.php?target={telegram_chat_id}&click_id={click_id}
Пример для Telegram-чата с ID -1003410571937:
https://example.com/tg-postback/subscribe.php?target=-1003410571937&click_id=abc123
Параметры:
| Параметр | Описание |
|---|---|
target |
Числовой Telegram ID канала или чата, включая начальный - |
click_id |
Click ID трекера длиной от 1 до 255 символов; передавайте его с URL-кодированием |
В настройках потока YellowTDS укажите прямой URL редиректа:
https://example.com/tg-postback/subscribe.php?target=-1003410571937&click_id={clickid}
YellowTDS подставит значение {clickid} и отправит клик напрямую в subscribe.php. Для другого трекера замените {clickid} на его макрос click ID. Значение target — постоянный Telegram ID подключённого канала или чата; готовую ссылку можно скопировать из панели.
При успешном запросе endpoint отвечает HTTP-редиректом на Telegram. Ошибка параметров возвращает 400, неизвестная или выключенная цель — 404, ошибка Telegram API при создании ссылки — 502, прочая ошибка приложения — 500.
Postback отправляется GET-запросом после подтверждённого вступления. Минимальный универсальный шаблон:
https://tracker.example/postback?click_id={clickid}&status={status}
Доступные макросы:
| Макрос | Значение |
|---|---|
{clickid} |
Значение входного параметра click_id |
{status} |
Статус конверсии; при вступлении — lead |
{user_id} |
Числовой Telegram ID пользователя |
{first_name} |
Имя |
{last_name} |
Фамилия, если указана |
{username} |
Telegram username без @, если указан |
{is_premium} |
true или false |
{language_code} |
Код языка Telegram, например ru или en |
Значения автоматически URL-кодируются. Если Telegram не передал необязательное поле, соответствующий макрос заменяется пустой строкой.
Пример расширенного шаблона:
https://tracker.example/postback?click_id={clickid}&status={status}&telegram_id={user_id}&name={first_name}&username={username}&premium={is_premium}&lang={language_code}
При неуспешной доставке система делает не более трёх попыток: сразу, затем после пауз 1 и 2 секунды. Таймаут каждой попытки — 5 секунд. Результат и число попыток сохраняются в SQLite. После успешной доставки повторно полученный Telegram update не отправляет тот же postback ещё раз.
Проверка синтаксиса и автоматических сценариев:
find app public tests -name '*.php' -print0 | xargs -0 -n1 php -l
php tests/run.phpПроверка публичного endpoint без перехода в Telegram:
curl -sS -D - -o /dev/null \
"https://example.com/tg-postback/subscribe.php?target=-1003410571937&click_id=readme-test-001"Ожидается HTTP-редирект с заголовком Location: https://t.me/....
Проверка webhook зарегистрированного бота:
curl -sS "https://api.telegram.org/bot<BOT_TOKEN>/getWebhookInfo"В ответе проверьте:
urlсовпадает сAPP_URLи заканчивается наwebhook.php?...;pending_update_countне растёт постоянно;last_error_messageотсутствует.
Не публикуйте вывод команды с токеном или закрытыми URL.
- Откройте ссылку с новым тестовым
click_id. - Убедитесь, что браузер перенаправлен на персональный инвайт Telegram.
- Вступите тестовым пользователем, который ещё не состоит в цели.
- Проверьте новую конверсию в трекере.
- Проверьте счётчик подписок в панели и
postback.log. - Повторите исходную ссылку: новой конверсии быть не должно, переход должен идти по fallback.
Журналы находятся в LOG_PATH:
| Файл | Содержимое |
|---|---|
app.log |
Создание и изменение сущностей, инвайты, служебные события |
webhook.log |
Полученные события вступления |
postback.log |
Попытки и успешная доставка postback |
errors.log |
Ошибки Telegram API, postback, базы и авторизации |
Просмотр в реальном времени:
tail -f logs/app.log logs/webhook.log logs/postback.log logs/errors.logОбщая диагностика PHP, расширений, конфигурации, SQLite и прав записи:
php test.phpДля необязательной проверки Telegram API передайте токен только через окружение:
TELEGRAM_TEST_TOKEN='123456:secret' php test.phpВ DB_PATH указана база от другой установки. Схема создаётся автоматически только в пустой базе. Если по указанному пути уже есть непустой файл SQLite без таблиц проекта, сохраните его отдельно и укажите новый путь. Автоматических миграций нет.
Цель не добавляется. Проверьте токен, членство бота, статус администратора, право приглашать пользователей и корректность @username либо числового chat ID.
Ссылка возвращает 400. Используйте только target и click_id. Убедитесь, что target — числовой Telegram ID канала или чата с начальным -, а click ID URL-кодирован.
Ссылка или панель возвращает 404 в подпапке. Проверьте одинаковый префикс в APP_URL, Nginx/Apache location и фактический путь к public/.
Telegram открывается, но конверсии нет. Пользователь должен вступить впервые именно по персональному инвайту. Проверьте getWebhookInfo, webhook.log, активность бота и цели.
Webhook получает ошибку. Адрес должен быть доступен Telegram по HTTPS без HTTP-авторизации и межсетевых ограничений. Сверьте URL из getWebhookInfo с APP_URL.
APP_URL был изменён после настройки. Сохраните каждого бота в панели ещё раз, чтобы зарегистрировать webhook с новым адресом.
Postback не приходит. Проверьте итоговый URL и макрос {clickid}, доступность трекера с сервера и postback.log/errors.log. После трёх неудачных попыток автоматическая доставка прекращается.
Повторный клик после вступления не создаёт конверсию. Это штатное поведение: для пары target + click_id конверсия создаётся один раз, а дальнейшие клики используют fallback.
Для резервной копии достаточно сохранить:
.env;- файл из
DB_PATH; - конфигурацию веб-сервера.
Перед копированием активной SQLite остановите запись в приложение либо используйте штатную команду .backup утилиты sqlite3.
Текущая версия рассчитана на чистую схему и не выполняет миграции существующей базы. Перед установкой версии с другой схемой сохраните старую базу отдельно и укажите в DB_PATH путь к новой пустой базе: схема создастся автоматически при первом запросе.
app/ PHP-классы, модели и сервисы
public/ единственный document root
public/index.php панель администратора
public/subscribe.php публичный redirect endpoint
public/webhook.php Telegram webhook
tests/ автоматические проверки
logs/ журналы по умолчанию
database.sqlite SQLite по умолчанию


