fix privacy error

This commit is contained in:
machka pasla
2025-12-04 12:13:17 +03:00
parent 2971b9ad2e
commit 719369057f
4 changed files with 148 additions and 40 deletions
+7 -25
View File
@@ -24,6 +24,10 @@ from bot.utils.text_sanitizer import (
sanitize_username, sanitize_username,
username_for_display, username_for_display,
) )
from bot.utils.telegram_markup import (
is_profile_link_error,
remove_profile_link_buttons,
)
router = Router(name="admin_user_management_router") router = Router(name="admin_user_management_router")
USERNAME_REGEX = re.compile(r"^[a-zA-Z0-9_]{5,32}$") 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 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( async def _send_with_profile_link_fallback(
sender: Callable[..., Awaitable[Any]], sender: Callable[..., Awaitable[Any]],
*, *,
@@ -210,16 +193,15 @@ async def _send_with_profile_link_fallback(
try: try:
await sender(**send_kwargs) await sender(**send_kwargs)
except TelegramBadRequest as exc: except TelegramBadRequest as exc:
message = getattr(exc, "message", "") or str(exc) if not is_profile_link_error(exc):
if "BUTTON_USER_INVALID" not in message:
raise raise
logging.warning( logging.warning(
"Telegram rejected profile buttons for user %s: %s. Retrying without tg:// links.", "Telegram rejected profile buttons for user %s: %s. Retrying without tg:// links.",
user_id, 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 send_kwargs["reply_markup"] = fallback_markup
await sender(**send_kwargs) await sender(**send_kwargs)
+40 -8
View File
@@ -3,7 +3,7 @@ import asyncio
from aiogram import Bot from aiogram import Bot
from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup
from aiogram.utils.text_decorations import html_decoration as hd 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 datetime import datetime, timezone
from typing import Optional, Union, Dict, Any, Callable from typing import Optional, Union, Dict, Any, Callable
@@ -15,6 +15,10 @@ from bot.utils.text_sanitizer import (
display_name_or_fallback, display_name_or_fallback,
username_for_display, username_for_display,
) )
from bot.utils.telegram_markup import (
is_profile_link_error,
remove_profile_link_buttons,
)
class NotificationService: class NotificationService:
@@ -81,14 +85,42 @@ class NotificationService:
queue_manager = get_queue_manager() queue_manager = get_queue_manager()
if not queue_manager: if not queue_manager:
logging.warning("Message queue manager not available, falling back to direct send") 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: try:
await self.bot.send_message( await self.bot.send_message(**_build_kwargs(reply_markup))
chat_id=self.settings.LOG_CHAT_ID, except TelegramBadRequest as exc:
text=message, if is_profile_link_error(exc):
parse_mode="HTML", fallback_markup = remove_profile_link_buttons(reply_markup)
disable_web_page_preview=True, logging.warning(
reply_markup=reply_markup, "Telegram rejected profile buttons for log chat %s: %s. "
message_thread_id=thread_id or self.settings.LOG_THREAD_ID "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: except Exception as e:
logging.error(f"Failed to send notification to log channel {self.settings.LOG_CHAT_ID}: {e}") logging.error(f"Failed to send notification to log channel {self.settings.LOG_CHAT_ID}: {e}")
+62 -7
View File
@@ -5,6 +5,12 @@ from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from collections import deque from collections import deque
from aiogram import Bot from aiogram import Bot
from aiogram.exceptions import TelegramBadRequest
from bot.utils.telegram_markup import (
is_profile_link_error,
remove_profile_link_buttons,
)
@dataclass @dataclass
@@ -51,14 +57,31 @@ class MessageQueue:
message = self.queue.popleft() message = self.queue.popleft()
try: try:
await self._send_message(message) await self._send_message(message)
self.last_send_times.append(datetime.now()) self._record_send_time()
self.total_sent += 1
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: except Exception as e:
self.total_failed += 1 self.total_failed += 1
logging.error(f"Failed to send queued message to {message.chat_id}: {e}") 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: if time_since_last < self.delay_between_messages:
wait_time = self.delay_between_messages - time_since_last wait_time = self.delay_between_messages - time_since_last
await asyncio.sleep(wait_time) 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: async def _send_message(self, message: QueuedMessage) -> Any:
"""Send a single message - to be implemented by subclass""" """Send a single message - to be implemented by subclass"""
+39
View File
@@ -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)