fix privacy error
This commit is contained in:
@@ -24,6 +24,10 @@ from bot.utils.text_sanitizer import (
|
||||
sanitize_username,
|
||||
username_for_display,
|
||||
)
|
||||
from bot.utils.telegram_markup import (
|
||||
is_profile_link_error,
|
||||
remove_profile_link_buttons,
|
||||
)
|
||||
|
||||
router = Router(name="admin_user_management_router")
|
||||
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$")
|
||||
@@ -174,27 +178,6 @@ def get_user_card_keyboard(user_id: int, i18n_instance, lang: str,
|
||||
return builder
|
||||
|
||||
|
||||
def _remove_profile_link_buttons(
|
||||
markup: Optional[types.InlineKeyboardMarkup]) -> Optional[types.InlineKeyboardMarkup]:
|
||||
"""Drop buttons that rely on tg://user links to avoid BUTTON_USER_INVALID errors."""
|
||||
if not markup or not markup.inline_keyboard:
|
||||
return None
|
||||
|
||||
cleaned_rows = []
|
||||
for row in markup.inline_keyboard:
|
||||
filtered_row = [
|
||||
button for button in row
|
||||
if not (getattr(button, "url", None) and button.url.startswith("tg://user?id="))
|
||||
]
|
||||
if filtered_row:
|
||||
cleaned_rows.append(filtered_row)
|
||||
|
||||
if not cleaned_rows:
|
||||
return None
|
||||
|
||||
return types.InlineKeyboardMarkup(inline_keyboard=cleaned_rows)
|
||||
|
||||
|
||||
async def _send_with_profile_link_fallback(
|
||||
sender: Callable[..., Awaitable[Any]],
|
||||
*,
|
||||
@@ -210,16 +193,15 @@ async def _send_with_profile_link_fallback(
|
||||
try:
|
||||
await sender(**send_kwargs)
|
||||
except TelegramBadRequest as exc:
|
||||
message = getattr(exc, "message", "") or str(exc)
|
||||
if "BUTTON_USER_INVALID" not in message:
|
||||
if not is_profile_link_error(exc):
|
||||
raise
|
||||
|
||||
logging.warning(
|
||||
"Telegram rejected profile buttons for user %s: %s. Retrying without tg:// links.",
|
||||
user_id,
|
||||
message,
|
||||
getattr(exc, "message", "") or str(exc),
|
||||
)
|
||||
fallback_markup = _remove_profile_link_buttons(markup)
|
||||
fallback_markup = remove_profile_link_buttons(markup)
|
||||
send_kwargs["reply_markup"] = fallback_markup
|
||||
await sender(**send_kwargs)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import asyncio
|
||||
from aiogram import Bot
|
||||
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
|
||||
from aiogram.utils.text_decorations import html_decoration as hd
|
||||
from aiogram.exceptions import TelegramRetryAfter
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Union, Dict, Any, Callable
|
||||
|
||||
@@ -15,6 +15,10 @@ from bot.utils.text_sanitizer import (
|
||||
display_name_or_fallback,
|
||||
username_for_display,
|
||||
)
|
||||
from bot.utils.telegram_markup import (
|
||||
is_profile_link_error,
|
||||
remove_profile_link_buttons,
|
||||
)
|
||||
|
||||
|
||||
class NotificationService:
|
||||
@@ -81,14 +85,42 @@ class NotificationService:
|
||||
queue_manager = get_queue_manager()
|
||||
if not queue_manager:
|
||||
logging.warning("Message queue manager not available, falling back to direct send")
|
||||
final_thread_id = thread_id or self.settings.LOG_THREAD_ID
|
||||
|
||||
def _build_kwargs(markup: Optional[InlineKeyboardMarkup]) -> Dict[str, Any]:
|
||||
kwargs: Dict[str, Any] = {
|
||||
"chat_id": self.settings.LOG_CHAT_ID,
|
||||
"text": message,
|
||||
"parse_mode": "HTML",
|
||||
"disable_web_page_preview": True,
|
||||
}
|
||||
if markup:
|
||||
kwargs["reply_markup"] = markup
|
||||
if final_thread_id:
|
||||
kwargs["message_thread_id"] = final_thread_id
|
||||
return kwargs
|
||||
|
||||
try:
|
||||
await self.bot.send_message(
|
||||
chat_id=self.settings.LOG_CHAT_ID,
|
||||
text=message,
|
||||
parse_mode="HTML",
|
||||
disable_web_page_preview=True,
|
||||
reply_markup=reply_markup,
|
||||
message_thread_id=thread_id or self.settings.LOG_THREAD_ID
|
||||
await self.bot.send_message(**_build_kwargs(reply_markup))
|
||||
except TelegramBadRequest as exc:
|
||||
if is_profile_link_error(exc):
|
||||
fallback_markup = remove_profile_link_buttons(reply_markup)
|
||||
logging.warning(
|
||||
"Telegram rejected profile buttons for log chat %s: %s. "
|
||||
"Retrying without tg:// links.",
|
||||
self.settings.LOG_CHAT_ID,
|
||||
getattr(exc, "message", "") or str(exc),
|
||||
)
|
||||
try:
|
||||
await self.bot.send_message(**_build_kwargs(fallback_markup))
|
||||
except Exception as retry_exc:
|
||||
logging.error(
|
||||
"Failed to send notification without profile buttons to log "
|
||||
f"channel {self.settings.LOG_CHAT_ID}: {retry_exc}"
|
||||
)
|
||||
return
|
||||
logging.error(
|
||||
f"Failed to send notification to log channel {self.settings.LOG_CHAT_ID}: {exc}"
|
||||
)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send notification to log channel {self.settings.LOG_CHAT_ID}: {e}")
|
||||
|
||||
@@ -5,6 +5,12 @@ from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from collections import deque
|
||||
from aiogram import Bot
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
|
||||
from bot.utils.telegram_markup import (
|
||||
is_profile_link_error,
|
||||
remove_profile_link_buttons,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -51,14 +57,31 @@ class MessageQueue:
|
||||
message = self.queue.popleft()
|
||||
try:
|
||||
await self._send_message(message)
|
||||
self.last_send_times.append(datetime.now())
|
||||
self.total_sent += 1
|
||||
self._record_send_time()
|
||||
|
||||
except TelegramBadRequest as exc:
|
||||
fallback_message = self._build_profile_link_fallback(message, exc)
|
||||
if fallback_message:
|
||||
logging.warning(
|
||||
"Telegram rejected profile buttons for chat %s: %s. "
|
||||
"Retrying without tg:// links.",
|
||||
message.chat_id,
|
||||
getattr(exc, "message", "") or str(exc),
|
||||
)
|
||||
try:
|
||||
await self._send_message(fallback_message)
|
||||
self._record_send_time()
|
||||
continue
|
||||
except Exception as retry_exc:
|
||||
self.total_failed += 1
|
||||
logging.error(
|
||||
f"Failed to send fallback message to {message.chat_id}: {retry_exc}"
|
||||
)
|
||||
continue
|
||||
|
||||
self.total_failed += 1
|
||||
logging.error(f"Failed to send queued message to {message.chat_id}: {exc}")
|
||||
|
||||
# Keep only recent send times (last minute)
|
||||
cutoff_time = datetime.now() - timedelta(seconds=60)
|
||||
while self.last_send_times and self.last_send_times[0] < cutoff_time:
|
||||
self.last_send_times.popleft()
|
||||
|
||||
except Exception as e:
|
||||
self.total_failed += 1
|
||||
logging.error(f"Failed to send queued message to {message.chat_id}: {e}")
|
||||
@@ -77,6 +100,38 @@ class MessageQueue:
|
||||
if time_since_last < self.delay_between_messages:
|
||||
wait_time = self.delay_between_messages - time_since_last
|
||||
await asyncio.sleep(wait_time)
|
||||
|
||||
def _record_send_time(self) -> None:
|
||||
"""Track sent message timestamps and purge old entries for rate limiting."""
|
||||
now = datetime.now()
|
||||
self.last_send_times.append(now)
|
||||
self.total_sent += 1
|
||||
|
||||
cutoff_time = now - timedelta(seconds=60)
|
||||
while self.last_send_times and self.last_send_times[0] < cutoff_time:
|
||||
self.last_send_times.popleft()
|
||||
|
||||
def _build_profile_link_fallback(
|
||||
self, message: QueuedMessage, exc: Exception
|
||||
) -> Optional[QueuedMessage]:
|
||||
"""Create a fallback message without tg://user buttons when Telegram rejects them."""
|
||||
if not is_profile_link_error(exc):
|
||||
return None
|
||||
|
||||
markup = message.kwargs.get("reply_markup")
|
||||
if markup is None:
|
||||
return None
|
||||
|
||||
safe_markup = remove_profile_link_buttons(markup)
|
||||
fallback_kwargs = dict(message.kwargs)
|
||||
fallback_kwargs["reply_markup"] = safe_markup
|
||||
|
||||
return QueuedMessage(
|
||||
chat_id=message.chat_id,
|
||||
method_name=message.method_name,
|
||||
kwargs=fallback_kwargs,
|
||||
callback=message.callback,
|
||||
)
|
||||
|
||||
async def _send_message(self, message: QueuedMessage) -> Any:
|
||||
"""Send a single message - to be implemented by subclass"""
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
from typing import Optional
|
||||
|
||||
from aiogram import types
|
||||
|
||||
PROFILE_BUTTON_ERROR_CODES = ("BUTTON_USER_INVALID", "BUTTON_USER_PRIVACY_RESTRICTED")
|
||||
TG_USER_LINK_PREFIX = "tg://user?id="
|
||||
|
||||
|
||||
def remove_profile_link_buttons(
|
||||
markup: Optional[types.InlineKeyboardMarkup],
|
||||
) -> Optional[types.InlineKeyboardMarkup]:
|
||||
"""Remove buttons that point to tg://user links to avoid privacy-related errors."""
|
||||
inline_keyboard = getattr(markup, "inline_keyboard", None)
|
||||
if not markup or not inline_keyboard:
|
||||
return None
|
||||
|
||||
cleaned_rows = []
|
||||
for row in inline_keyboard:
|
||||
filtered_row = [
|
||||
button
|
||||
for button in row
|
||||
if not (
|
||||
getattr(button, "url", None)
|
||||
and str(button.url).startswith(TG_USER_LINK_PREFIX)
|
||||
)
|
||||
]
|
||||
if filtered_row:
|
||||
cleaned_rows.append(filtered_row)
|
||||
|
||||
if not cleaned_rows:
|
||||
return None
|
||||
|
||||
return types.InlineKeyboardMarkup(inline_keyboard=cleaned_rows)
|
||||
|
||||
|
||||
def is_profile_link_error(exc: BaseException) -> bool:
|
||||
"""Return True if Telegram rejected markup because of profile link buttons."""
|
||||
message = getattr(exc, "message", "") or str(exc)
|
||||
return any(code in message for code in PROFILE_BUTTON_ERROR_CODES)
|
||||
Reference in New Issue
Block a user