This commit is contained in:
+65
-45
@@ -2,7 +2,6 @@ import os
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
import errno
|
import errno
|
||||||
import asyncio
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
import telebot
|
import telebot
|
||||||
from telebot import apihelper
|
from telebot import apihelper
|
||||||
@@ -15,13 +14,15 @@ import requests
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from flask import Flask, request
|
from flask import Flask, request
|
||||||
from formatter import get_post_text, get_event_text
|
from formatter import get_post_text, get_event_text
|
||||||
from telegram_relay import configure_telebot_api, get_relay_url
|
from telegram_relay import configure_telebot_api, get_relay_url, bot_api_method_url
|
||||||
|
|
||||||
# Создаем Flask app на верхнем уровне для экспорта
|
# Создаем Flask app на верхнем уровне для экспорта
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# Загружаем переменные окружения из файла .env
|
# Загружаем .env рядом с этим файлом (не зависим от cwd gunicorn)
|
||||||
load_dotenv()
|
_ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env")
|
||||||
|
load_dotenv(_ENV_PATH)
|
||||||
|
load_dotenv() # и из cwd на всякий случай
|
||||||
|
|
||||||
# Получаем данные из переменных окружения
|
# Получаем данные из переменных окружения
|
||||||
RESPONDER_BOT_TOKEN = os.getenv('RESPONDER_BOT_TOKEN')
|
RESPONDER_BOT_TOKEN = os.getenv('RESPONDER_BOT_TOKEN')
|
||||||
@@ -1437,22 +1438,58 @@ def handle_callback(call):
|
|||||||
|
|
||||||
def setup_webhook():
|
def setup_webhook():
|
||||||
"""
|
"""
|
||||||
Настройка вебхука через ретранслятор — как в retransbot/testunit/echobot.py:
|
Регистрация webhook через internal_relay.
|
||||||
python-telegram-bot Bot.set_webhook(url=..., secret_token=...).
|
|
||||||
PTB передаёт параметры form-urlencoded (httpx data=), и relay их корректно
|
Важно: relay перехватывает setWebhook и уносит secret_token в Telegram через
|
||||||
пробрасывает в Telegram вместе с secret_token.
|
extra_params. Параметры нужно слать как application/x-www-form-urlencoded
|
||||||
|
(так делает echobot/PTB и так разбирает relay после фикса 0.1.1).
|
||||||
"""
|
"""
|
||||||
logger.info("starting setup_webhook")
|
logger.info("starting setup_webhook")
|
||||||
last_error = None
|
if not WEBHOOK_SECRET:
|
||||||
|
raise RuntimeError(
|
||||||
|
"WEBHOOK_SECRET не задан — без него Telegram не будет присылать "
|
||||||
|
"X-Telegram-Bot-Api-Secret-Token"
|
||||||
|
)
|
||||||
|
if not WEBHOOK_URL:
|
||||||
|
raise RuntimeError("WEBHOOK_URL не задан")
|
||||||
|
|
||||||
|
api_url = bot_api_method_url(RESPONDER_BOT_TOKEN, "setWebhook")
|
||||||
|
# Все значения — строки (form-urlencoded), как ожидает parse_qs на relay
|
||||||
|
form = {
|
||||||
|
"url": WEBHOOK_URL,
|
||||||
|
"secret_token": WEBHOOK_SECRET,
|
||||||
|
"max_connections": "40",
|
||||||
|
}
|
||||||
|
logger.info(
|
||||||
|
f"setWebhook через relay: delivery_url={WEBHOOK_URL}, "
|
||||||
|
f"secret_len={len(WEBHOOK_SECRET)}, relay={get_relay_url()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
last_error = None
|
||||||
for attempt in range(1, 6):
|
for attempt in range(1, 6):
|
||||||
try:
|
try:
|
||||||
asyncio.run(_setup_webhook_ptb())
|
resp = requests.post(
|
||||||
|
api_url,
|
||||||
|
data=form,
|
||||||
|
timeout=60,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
except Exception:
|
||||||
|
data = {"ok": False, "description": (resp.text or "")[:500]}
|
||||||
|
logger.info(f"setWebhook ответ HTTP {resp.status_code}: {data}")
|
||||||
|
|
||||||
|
desc = str(data.get("description", "")) if isinstance(data, dict) else ""
|
||||||
|
if resp.status_code == 429 or "retry after" in desc.lower():
|
||||||
|
raise RuntimeError(desc or f"HTTP {resp.status_code}")
|
||||||
|
if not (isinstance(data, dict) and data.get("ok")):
|
||||||
|
raise RuntimeError(f"setWebhook failed: HTTP {resp.status_code} {data}")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Вебхук установлен через ретранслятор {get_relay_url()}: "
|
f"Вебхук установлен через ретранслятор {get_relay_url()}: "
|
||||||
f"локальная доставка {WEBHOOK_URL}"
|
f"локальная доставка {WEBHOOK_URL}"
|
||||||
)
|
)
|
||||||
logger.info(f"Секретный токен: {'установлен' if WEBHOOK_SECRET else 'не установлен'}")
|
logger.info(f"Секретный токен: установлен (len={len(WEBHOOK_SECRET)})")
|
||||||
logger.info(f"Прослушивание порта: {WEBHOOK_PORT}")
|
logger.info(f"Прослушивание порта: {WEBHOOK_PORT}")
|
||||||
logger.info(f"Путь вебхука: {WEBHOOK_PATH}")
|
logger.info(f"Путь вебхука: {WEBHOOK_PATH}")
|
||||||
log_event("system", "system", message=f"Вебхук установлен на {WEBHOOK_URL}")
|
log_event("system", "system", message=f"Вебхук установлен на {WEBHOOK_URL}")
|
||||||
@@ -1474,37 +1511,6 @@ def setup_webhook():
|
|||||||
raise RuntimeError(error_msg) from last_error
|
raise RuntimeError(error_msg) from last_error
|
||||||
|
|
||||||
|
|
||||||
async def _setup_webhook_ptb():
|
|
||||||
"""Регистрация webhook через PTB — тот же стек, что у проверенного echobot."""
|
|
||||||
from telegram import Bot
|
|
||||||
from telegram.request import HTTPXRequest
|
|
||||||
from telegram_relay import get_ptb_base_urls
|
|
||||||
|
|
||||||
base_url, base_file_url = get_ptb_base_urls()
|
|
||||||
request_kw = HTTPXRequest(
|
|
||||||
connect_timeout=30.0,
|
|
||||||
read_timeout=60.0,
|
|
||||||
write_timeout=60.0,
|
|
||||||
pool_timeout=10.0,
|
|
||||||
)
|
|
||||||
tg_bot = Bot(
|
|
||||||
token=RESPONDER_BOT_TOKEN,
|
|
||||||
base_url=base_url,
|
|
||||||
base_file_url=base_file_url,
|
|
||||||
request=request_kw,
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
ok = await tg_bot.set_webhook(
|
|
||||||
url=WEBHOOK_URL,
|
|
||||||
secret_token=WEBHOOK_SECRET,
|
|
||||||
max_connections=40,
|
|
||||||
)
|
|
||||||
if not ok:
|
|
||||||
raise RuntimeError("set_webhook returned False")
|
|
||||||
finally:
|
|
||||||
await tg_bot.shutdown()
|
|
||||||
|
|
||||||
|
|
||||||
def _webhook_retry_after(exc):
|
def _webhook_retry_after(exc):
|
||||||
"""Извлекает секунды ожидания из 429 / 'retry after N', иначе None."""
|
"""Извлекает секунды ожидания из 429 / 'retry after N', иначе None."""
|
||||||
text = str(exc)
|
text = str(exc)
|
||||||
@@ -1517,20 +1523,34 @@ def _webhook_retry_after(exc):
|
|||||||
return 2
|
return 2
|
||||||
|
|
||||||
|
|
||||||
|
def _read_telegram_secret_header():
|
||||||
|
"""Читает секрет из заголовка (Flask headers и сырой WSGI environ)."""
|
||||||
|
return (
|
||||||
|
request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
||||||
|
or request.environ.get("HTTP_X_TELEGRAM_BOT_API_SECRET_TOKEN")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Используем путь из WEBHOOK_URL
|
# Используем путь из WEBHOOK_URL
|
||||||
@app.route(WEBHOOK_PATH, methods=['POST'])
|
@app.route(WEBHOOK_PATH, methods=['POST'])
|
||||||
def webhook():
|
def webhook():
|
||||||
logger.info("starting webhook")
|
logger.info("starting webhook")
|
||||||
received_secret = request.headers.get("X-Telegram-Bot-Api-Secret-Token")
|
received_secret = _read_telegram_secret_header()
|
||||||
if received_secret != WEBHOOK_SECRET:
|
if received_secret != WEBHOOK_SECRET:
|
||||||
|
related = [
|
||||||
|
k for k in request.headers.keys()
|
||||||
|
if "secret" in k.lower() or "telegram" in k.lower() or k.lower().startswith("x-")
|
||||||
|
]
|
||||||
if not received_secret:
|
if not received_secret:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Секретный токен отсутствует в заголовке X-Telegram-Bot-Api-Secret-Token"
|
"Секретный токен отсутствует в заголовке X-Telegram-Bot-Api-Secret-Token; "
|
||||||
|
f"IP={request.remote_addr}; related_headers={related}"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Секретный токен не совпадает "
|
"Секретный токен не совпадает "
|
||||||
f"(получен len={len(received_secret)}, ожидается len={len(WEBHOOK_SECRET or '')})"
|
f"(получен len={len(received_secret)}, ожидается len={len(WEBHOOK_SECRET or '')}); "
|
||||||
|
f"related_headers={related}"
|
||||||
)
|
)
|
||||||
log_event("security", "system", message="Попытка доступа с неверным секретным токеном")
|
log_event("security", "system", message="Попытка доступа с неверным секретным токеном")
|
||||||
return "Unauthorized", 401
|
return "Unauthorized", 401
|
||||||
|
|||||||
Reference in New Issue
Block a user