diff --git a/app/database/models.py b/app/database/models.py
index c7b6465e..13cdfd0b 100644
--- a/app/database/models.py
+++ b/app/database/models.py
@@ -1553,22 +1553,6 @@ class WelcomeText(Base):
creator = relationship("User", backref="created_welcome_texts")
-class PinnedMessage(Base):
- __tablename__ = "pinned_messages"
-
- id = Column(Integer, primary_key=True, index=True)
- content = Column(Text, nullable=False, default="")
- media_type = Column(String(32), nullable=True)
- media_file_id = Column(String(255), nullable=True)
- send_before_menu = Column(Boolean, nullable=False, server_default="1", default=True)
- is_active = Column(Boolean, default=True)
- created_by = Column(Integer, ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
- created_at = Column(DateTime, default=func.now())
- updated_at = Column(DateTime, default=func.now(), onupdate=func.now())
-
- creator = relationship("User", backref="pinned_messages")
-
-
class AdvertisingCampaign(Base):
__tablename__ = "advertising_campaigns"
diff --git a/app/database/universal_migration.py b/app/database/universal_migration.py
index cf8d7eb8..e418502e 100644
--- a/app/database/universal_migration.py
+++ b/app/database/universal_migration.py
@@ -3014,132 +3014,6 @@ async def create_welcome_texts_table():
logger.error(f"Ошибка создания таблицы welcome_texts: {e}")
return False
-
-async def create_pinned_messages_table():
- table_exists = await check_table_exists("pinned_messages")
- if table_exists:
- logger.info("Таблица pinned_messages уже существует")
- return True
-
- try:
- async with engine.begin() as conn:
- db_type = await get_database_type()
-
- if db_type == "sqlite":
- create_sql = """
- CREATE TABLE pinned_messages (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- content TEXT NOT NULL DEFAULT '',
- media_type VARCHAR(32) NULL,
- media_file_id VARCHAR(255) NULL,
- send_before_menu BOOLEAN NOT NULL DEFAULT 1,
- is_active BOOLEAN DEFAULT 1,
- created_by INTEGER NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
- );
-
- CREATE INDEX IF NOT EXISTS ix_pinned_messages_active ON pinned_messages(is_active);
- """
-
- elif db_type == "postgresql":
- create_sql = """
- CREATE TABLE pinned_messages (
- id SERIAL PRIMARY KEY,
- content TEXT NOT NULL DEFAULT '',
- media_type VARCHAR(32) NULL,
- media_file_id VARCHAR(255) NULL,
- send_before_menu BOOLEAN NOT NULL DEFAULT TRUE,
- is_active BOOLEAN DEFAULT TRUE,
- created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- );
-
- CREATE INDEX IF NOT EXISTS ix_pinned_messages_active ON pinned_messages(is_active);
- """
-
- elif db_type == "mysql":
- create_sql = """
- CREATE TABLE pinned_messages (
- id INT AUTO_INCREMENT PRIMARY KEY,
- content TEXT NOT NULL DEFAULT '',
- media_type VARCHAR(32) NULL,
- media_file_id VARCHAR(255) NULL,
- send_before_menu BOOLEAN NOT NULL DEFAULT TRUE,
- is_active BOOLEAN DEFAULT TRUE,
- created_by INT NULL,
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
- FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
- );
-
- CREATE INDEX ix_pinned_messages_active ON pinned_messages(is_active);
- """
-
- else:
- logger.error(f"Неподдерживаемый тип БД для создания таблицы pinned_messages: {db_type}")
- return False
-
- await conn.execute(text(create_sql))
-
- logger.info("✅ Таблица pinned_messages успешно создана")
- return True
-
- except Exception as e:
- logger.error(f"Ошибка создания таблицы pinned_messages: {e}")
- return False
-
-
-async def ensure_pinned_message_media_columns():
- table_exists = await check_table_exists("pinned_messages")
- if not table_exists:
- logger.warning("⚠️ Таблица pinned_messages отсутствует — пропускаем обновление медиа полей")
- return False
-
- try:
- async with engine.begin() as conn:
- db_type = await get_database_type()
-
- if not await check_column_exists("pinned_messages", "media_type"):
- await conn.execute(
- text("ALTER TABLE pinned_messages ADD COLUMN media_type VARCHAR(32)")
- )
-
- if not await check_column_exists("pinned_messages", "media_file_id"):
- await conn.execute(
- text("ALTER TABLE pinned_messages ADD COLUMN media_file_id VARCHAR(255)")
- )
-
- if not await check_column_exists("pinned_messages", "send_before_menu"):
- default_value = "TRUE" if db_type != "sqlite" else "1"
- await conn.execute(
- text(
- f"ALTER TABLE pinned_messages ADD COLUMN send_before_menu BOOLEAN NOT NULL DEFAULT {default_value}"
- )
- )
-
- await conn.execute(text("UPDATE pinned_messages SET content = '' WHERE content IS NULL"))
-
- if db_type == "postgresql":
- await conn.execute(
- text("ALTER TABLE pinned_messages ALTER COLUMN content SET DEFAULT ''")
- )
- elif db_type == "mysql":
- await conn.execute(
- text("ALTER TABLE pinned_messages MODIFY content TEXT NOT NULL DEFAULT ''")
- )
- else:
- logger.info("ℹ️ Пропускаем установку DEFAULT для content в SQLite")
-
- logger.info("✅ Медиа поля pinned_messages приведены в актуальное состояние")
- return True
-
- except Exception as e:
- logger.error(f"Ошибка обновления медиа полей pinned_messages: {e}")
- return False
-
async def add_media_fields_to_broadcast_history():
logger.info("=== ДОБАВЛЕНИЕ ПОЛЕЙ МЕДИА В BROADCAST_HISTORY ===")
@@ -4816,26 +4690,12 @@ async def run_universal_migration():
else:
logger.warning("⚠️ Проблемы с таблицей user_messages")
- logger.info("=== СОЗДАНИЕ ТАБЛИЦЫ PINNED_MESSAGES ===")
- pinned_messages_created = await create_pinned_messages_table()
- if pinned_messages_created:
- logger.info("✅ Таблица pinned_messages готова")
- else:
- logger.warning("⚠️ Проблемы с таблицей pinned_messages")
-
logger.info("=== СОЗДАНИЕ/ОБНОВЛЕНИЕ ТАБЛИЦЫ WELCOME_TEXTS ===")
welcome_texts_created = await create_welcome_texts_table()
if welcome_texts_created:
logger.info("✅ Таблица welcome_texts готова с полем is_enabled")
else:
logger.warning("⚠️ Проблемы с таблицей welcome_texts")
-
- logger.info("=== ОБНОВЛЕНИЕ СХЕМЫ PINNED_MESSAGES ===")
- pinned_media_ready = await ensure_pinned_message_media_columns()
- if pinned_media_ready:
- logger.info("✅ Медиа поля для pinned_messages готовы")
- else:
- logger.warning("⚠️ Проблемы с медиа полями pinned_messages")
logger.info("=== ДОБАВЛЕНИЕ МЕДИА ПОЛЕЙ В BROADCAST_HISTORY ===")
media_fields_added = await add_media_fields_to_broadcast_history()
@@ -5020,11 +4880,8 @@ async def check_migration_status():
"cryptobot_table": False,
"heleket_table": False,
"user_messages_table": False,
- "pinned_messages_table": False,
"welcome_texts_table": False,
"welcome_texts_is_enabled_column": False,
- "pinned_messages_media_columns": False,
- "pinned_messages_position_column": False,
"broadcast_history_media_fields": False,
"subscription_duplicates": False,
"subscription_conversions_table": False,
@@ -5067,7 +4924,6 @@ async def check_migration_status():
status["cryptobot_table"] = await check_table_exists('cryptobot_payments')
status["heleket_table"] = await check_table_exists('heleket_payments')
status["user_messages_table"] = await check_table_exists('user_messages')
- status["pinned_messages_table"] = await check_table_exists('pinned_messages')
status["welcome_texts_table"] = await check_table_exists('welcome_texts')
status["privacy_policies_table"] = await check_table_exists('privacy_policies')
status["public_offers_table"] = await check_table_exists('public_offers')
@@ -5113,18 +4969,6 @@ async def check_migration_status():
await check_column_exists('broadcast_history', 'media_caption')
)
status["broadcast_history_media_fields"] = media_fields_exist
-
- pinned_media_columns_exist = (
- status["pinned_messages_table"]
- and await check_column_exists('pinned_messages', 'media_type')
- and await check_column_exists('pinned_messages', 'media_file_id')
- )
- status["pinned_messages_media_columns"] = pinned_media_columns_exist
-
- status["pinned_messages_position_column"] = (
- status["pinned_messages_table"]
- and await check_column_exists('pinned_messages', 'send_before_menu')
- )
async with engine.begin() as conn:
duplicates_check = await conn.execute(text("""
@@ -5143,13 +4987,10 @@ async def check_migration_status():
"cryptobot_table": "Таблица CryptoBot payments",
"heleket_table": "Таблица Heleket payments",
"user_messages_table": "Таблица пользовательских сообщений",
- "pinned_messages_table": "Таблица закреплённых сообщений",
"welcome_texts_table": "Таблица приветственных текстов",
"privacy_policies_table": "Таблица политик конфиденциальности",
"public_offers_table": "Таблица публичных оферт",
"welcome_texts_is_enabled_column": "Поле is_enabled в welcome_texts",
- "pinned_messages_media_columns": "Медиа поля в pinned_messages",
- "pinned_messages_position_column": "Позиция закрепа (до/после меню)",
"broadcast_history_media_fields": "Медиа поля в broadcast_history",
"subscription_conversions_table": "Таблица конверсий подписок",
"subscription_events_table": "Таблица событий подписок",
diff --git a/app/handlers/admin/messages.py b/app/handlers/admin/messages.py
index e6bd434a..3bf0210f 100644
--- a/app/handlers/admin/messages.py
+++ b/app/handlers/admin/messages.py
@@ -1,4 +1,3 @@
-import html
import logging
import asyncio
from datetime import datetime, timedelta
@@ -26,18 +25,13 @@ from app.keyboards.admin import (
get_admin_pagination_keyboard, get_broadcast_media_keyboard,
get_media_confirm_keyboard, get_updated_message_buttons_selector_keyboard_with_media,
BROADCAST_BUTTON_ROWS, DEFAULT_BROADCAST_BUTTONS,
- get_broadcast_button_config, get_broadcast_button_labels, get_pinned_message_keyboard
+ get_broadcast_button_config, get_broadcast_button_labels
)
from app.localization.texts import get_texts
from app.database.crud.user import get_users_list
from app.database.crud.subscription import get_expiring_subscriptions
from app.utils.decorators import admin_required, error_handler
from app.utils.miniapp_buttons import build_miniapp_or_callback_button
-from app.services.pinned_message_service import (
- broadcast_pinned_message,
- get_active_pinned_message,
- set_active_pinned_message,
-)
logger = logging.getLogger(__name__)
@@ -173,154 +167,6 @@ async def show_messages_menu(
await callback.answer()
-@admin_required
-@error_handler
-async def show_pinned_message_menu(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- await state.clear()
- pinned_message = await get_active_pinned_message(db)
-
- if pinned_message:
- content_preview = html.escape(pinned_message.content or "")
- last_updated = pinned_message.updated_at or pinned_message.created_at
- timestamp_text = last_updated.strftime("%d.%m.%Y %H:%M") if last_updated else "—"
- media_line = ""
- if pinned_message.media_type:
- media_label = "Фото" if pinned_message.media_type == "photo" else "Видео"
- media_line = f"📎 Медиа: {media_label}\n"
- position_line = (
- "⬆️ Отправлять перед меню"
- if pinned_message.send_before_menu
- else "⬇️ Отправлять после меню"
- )
- body = (
- "📌 Закрепленное сообщение\n\n"
- "📝 Текущий текст:\n"
- f"{content_preview}\n\n"
- f"{media_line}"
- f"{position_line}\n"
- f"🕒 Обновлено: {timestamp_text}"
- )
- else:
- body = (
- "📌 Закрепленное сообщение\n\n"
- "Сообщение не задано. Отправьте новый текст, чтобы разослать и закрепить его у пользователей."
- )
-
- await callback.message.edit_text(
- body,
- reply_markup=get_pinned_message_keyboard(
- db_user.language,
- send_before_menu=getattr(pinned_message, "send_before_menu", True),
- ),
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def prompt_pinned_message_update(
- callback: types.CallbackQuery,
- db_user: User,
- state: FSMContext,
-):
- await state.set_state(AdminStates.editing_pinned_message)
- await callback.message.edit_text(
- "✏️ Новое закрепленное сообщение\n\n"
- "Пришлите текст, фото или видео, которое нужно закрепить.\n"
- "Бот отправит его всем активным пользователям, открепит старое и закрепит новое без уведомлений.",
- reply_markup=types.InlineKeyboardMarkup(inline_keyboard=[
- [types.InlineKeyboardButton(text="❌ Отмена", callback_data="admin_pinned_message")]
- ]),
- parse_mode="HTML",
- )
- await callback.answer()
-
-
-@admin_required
-@error_handler
-async def toggle_pinned_message_position(
- callback: types.CallbackQuery,
- db_user: User,
- db: AsyncSession,
- state: FSMContext,
-):
- pinned_message = await get_active_pinned_message(db)
- if not pinned_message:
- await callback.answer("Сначала задайте закрепленное сообщение", show_alert=True)
- return
-
- pinned_message.send_before_menu = not pinned_message.send_before_menu
- pinned_message.updated_at = datetime.utcnow()
- await db.commit()
-
- await show_pinned_message_menu(callback, db_user, db, state)
-
-
-@admin_required
-@error_handler
-async def process_pinned_message_update(
- message: types.Message,
- db_user: User,
- state: FSMContext,
- db: AsyncSession,
-):
- media_type: Optional[str] = None
- media_file_id: Optional[str] = None
-
- if message.photo:
- media_type = "photo"
- media_file_id = message.photo[-1].file_id
- elif message.video:
- media_type = "video"
- media_file_id = message.video.file_id
-
- pinned_text = message.html_text or message.caption_html or message.text or message.caption or ""
-
- if not pinned_text and not media_file_id:
- await message.answer("❌ Не удалось прочитать текст или медиа в сообщении, попробуйте снова.")
- return
-
- try:
- pinned_message = await set_active_pinned_message(
- db,
- pinned_text,
- db_user.id,
- media_type=media_type,
- media_file_id=media_file_id,
- )
- except ValueError as validation_error:
- await message.answer(f"❌ {validation_error}")
- return
-
- await message.answer(
- "📌 Сообщение сохранено. Начинаю отправку и закрепление у пользователей...",
- parse_mode="HTML",
- )
-
- sent_count, failed_count = await broadcast_pinned_message(
- message.bot,
- db,
- pinned_message,
- )
-
- total = sent_count + failed_count
- await message.answer(
- "✅ Закрепленное сообщение обновлено\n\n"
- f"👥 Получателей: {total}\n"
- f"✅ Отправлено: {sent_count}\n"
- f"⚠️ Ошибок: {failed_count}",
- reply_markup=get_admin_messages_keyboard(db_user.language),
- parse_mode="HTML",
- )
- await state.clear()
-
-
@admin_required
@error_handler
async def show_broadcast_targets(
@@ -1449,9 +1295,6 @@ def get_target_display_name(target: str) -> str:
def register_handlers(dp: Dispatcher):
dp.callback_query.register(show_messages_menu, F.data == "admin_messages")
- dp.callback_query.register(show_pinned_message_menu, F.data == "admin_pinned_message")
- dp.callback_query.register(toggle_pinned_message_position, F.data == "admin_pinned_message_position")
- dp.callback_query.register(prompt_pinned_message_update, F.data == "admin_pinned_message_edit")
dp.callback_query.register(show_broadcast_targets, F.data.in_(["admin_msg_all", "admin_msg_by_sub"]))
dp.callback_query.register(select_broadcast_target, F.data.startswith("broadcast_"))
dp.callback_query.register(confirm_broadcast, F.data == "admin_confirm_broadcast")
@@ -1469,4 +1312,3 @@ def register_handlers(dp: Dispatcher):
dp.callback_query.register(handle_change_media, F.data == "change_media")
dp.message.register(process_broadcast_message, AdminStates.waiting_for_broadcast_message)
dp.message.register(process_broadcast_media, AdminStates.waiting_for_broadcast_media)
- dp.message.register(process_pinned_message_update, AdminStates.editing_pinned_message)
diff --git a/app/handlers/common.py b/app/handlers/common.py
index 7d389163..3c8549f2 100644
--- a/app/handlers/common.py
+++ b/app/handlers/common.py
@@ -67,7 +67,7 @@ async def handle_cancel(
async def handle_unknown_message(
message: types.Message,
- db_user: User | None = None,
+ db_user: User
):
texts = get_texts(db_user.language if db_user else "ru")
@@ -126,8 +126,6 @@ def register_handlers(dp: Dispatcher):
dp.message.register(
handle_unknown_message,
StateFilter(None),
- F.successful_payment.is_(None),
- F.text.is_not(None),
- ~F.text.startswith("/"),
+ F.successful_payment.is_(None)
)
\ No newline at end of file
diff --git a/app/handlers/start.py b/app/handlers/start.py
index 026ce92f..f2e125d1 100644
--- a/app/handlers/start.py
+++ b/app/handlers/start.py
@@ -1,6 +1,5 @@
import logging
from datetime import datetime
-from typing import Optional
from aiogram import Dispatcher, types, F, Bot
from aiogram.enums import ChatMemberStatus
from aiogram.exceptions import TelegramForbiddenError
@@ -19,7 +18,7 @@ from app.database.crud.campaign import (
get_campaign_by_start_parameter,
get_campaign_by_id,
)
-from app.database.models import PinnedMessage, SubscriptionStatus, UserStatus
+from app.database.models import UserStatus, SubscriptionStatus
from app.keyboards.inline import (
get_rules_keyboard,
get_privacy_policy_keyboard,
@@ -37,10 +36,6 @@ from app.services.subscription_service import SubscriptionService
from app.services.support_settings_service import SupportSettingsService
from app.services.main_menu_button_service import MainMenuButtonService
from app.services.privacy_policy_service import PrivacyPolicyService
-from app.services.pinned_message_service import (
- deliver_pinned_message_to_user,
- get_active_pinned_message,
-)
from app.utils.user_utils import generate_unique_referral_code
from app.utils.promo_offer import (
build_promo_offer_hint,
@@ -66,22 +61,6 @@ def _calculate_subscription_flags(subscription):
return has_active_subscription, subscription_is_active
-async def _send_pinned_message(
- bot: Bot,
- db: AsyncSession,
- user,
- pinned_message: Optional[PinnedMessage] = None,
-) -> None:
- try:
- await deliver_pinned_message_to_user(bot, db, user, pinned_message)
- except Exception as error: # noqa: BLE001
- logger.error(
- "Не удалось отправить закрепленное сообщение пользователю %s: %s",
- getattr(user, "telegram_id", "unknown"),
- error,
- )
-
-
async def _apply_campaign_bonus_if_needed(
db: AsyncSession,
user,
@@ -425,11 +404,6 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
user.subscription
)
- pinned_message = await get_active_pinned_message(db)
-
- if pinned_message and pinned_message.send_before_menu:
- await _send_pinned_message(message.bot, db, user, pinned_message)
-
menu_text = await get_main_menu_text(user, texts, db)
is_admin = settings.is_admin(user.telegram_id)
@@ -464,9 +438,6 @@ async def cmd_start(message: types.Message, state: FSMContext, db: AsyncSession,
reply_markup=keyboard,
parse_mode="HTML"
)
-
- if pinned_message and not pinned_message.send_before_menu:
- await _send_pinned_message(message.bot, db, user, pinned_message)
await state.clear()
return
@@ -1123,7 +1094,6 @@ async def complete_registration_from_callback(
reply_markup=keyboard,
parse_mode="HTML"
)
- await _send_pinned_message(callback.bot, db, existing_user)
except Exception as e:
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
await callback.message.answer(
@@ -1262,7 +1232,6 @@ async def complete_registration_from_callback(
reply_markup=get_post_registration_keyboard(user.language),
)
logger.info(f"✅ Приветственное сообщение отправлено пользователю {user.telegram_id}")
- await _send_pinned_message(callback.bot, db, user)
except Exception as e:
logger.error(f"Ошибка при отправке приветственного сообщения: {e}")
else:
@@ -1308,7 +1277,6 @@ async def complete_registration_from_callback(
reply_markup=keyboard,
parse_mode="HTML"
)
- await _send_pinned_message(callback.bot, db, user)
logger.info(f"✅ Главное меню показано пользователю {user.telegram_id}")
except Exception as e:
logger.error(f"Ошибка при показе главного меню: {e}")
@@ -1406,7 +1374,6 @@ async def complete_registration(
reply_markup=keyboard,
parse_mode="HTML"
)
- await _send_pinned_message(message.bot, db, existing_user)
except Exception as e:
logger.error(f"Ошибка при показе главного меню существующему пользователю: {e}")
await message.answer(
@@ -1568,7 +1535,6 @@ async def complete_registration(
reply_markup=get_post_registration_keyboard(user.language),
)
logger.info(f"✅ Приветственное сообщение отправлено пользователю {user.telegram_id}")
- await _send_pinned_message(message.bot, db, user)
except Exception as e:
logger.error(f"Ошибка при отправке приветственного сообщения: {e}")
else:
@@ -1615,7 +1581,6 @@ async def complete_registration(
parse_mode="HTML"
)
logger.info(f"✅ Главное меню показано пользователю {user.telegram_id}")
- await _send_pinned_message(message.bot, db, user)
except Exception as e:
logger.error(f"Ошибка при показе главного меню: {e}")
await message.answer(
@@ -1960,7 +1925,6 @@ async def required_sub_channel_check(
reply_markup=keyboard,
parse_mode="HTML",
)
- await _send_pinned_message(bot, db, user)
else:
from app.keyboards.inline import get_rules_keyboard
diff --git a/app/keyboards/admin.py b/app/keyboards/admin.py
index deed1713..0ab2cd7f 100644
--- a/app/keyboards/admin.py
+++ b/app/keyboards/admin.py
@@ -837,45 +837,12 @@ def get_admin_messages_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
callback_data="admin_msg_history"
)
],
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_PINNED_MESSAGE", "📌 Закрепленное сообщение"),
- callback_data="admin_pinned_message",
- )
- ],
[
InlineKeyboardButton(text=texts.BACK, callback_data="admin_submenu_communications")
]
])
-def get_pinned_message_keyboard(language: str = "ru", send_before_menu: bool = True) -> InlineKeyboardMarkup:
- texts = get_texts(language)
-
- position_label = (
- _t(texts, "ADMIN_PINNED_POSITION_BEFORE", "⬆️ Показать перед меню")
- if send_before_menu
- else _t(texts, "ADMIN_PINNED_POSITION_AFTER", "⬇️ Показать после меню")
- )
- toggle_callback = "admin_pinned_message_position"
-
- return InlineKeyboardMarkup(inline_keyboard=[
- [
- InlineKeyboardButton(
- text=_t(texts, "ADMIN_PINNED_MESSAGE_UPDATE", "✏️ Обновить"),
- callback_data="admin_pinned_message_edit",
- )
- ],
- [
- InlineKeyboardButton(
- text=position_label,
- callback_data=toggle_callback,
- )
- ],
- [InlineKeyboardButton(text=texts.BACK, callback_data="admin_messages")],
- ])
-
-
def get_admin_monitoring_keyboard(language: str = "ru") -> InlineKeyboardMarkup:
texts = get_texts(language)
diff --git a/app/localization/locales/en.json b/app/localization/locales/en.json
index 7b7f6c1c..1c6bc309 100644
--- a/app/localization/locales/en.json
+++ b/app/localization/locales/en.json
@@ -209,10 +209,6 @@
"ADMIN_MESSAGES_BY_CRITERIA": "🔍 By criteria",
"ADMIN_MESSAGES_BY_SUBSCRIPTIONS": "🎯 By subscriptions",
"ADMIN_MESSAGES_HISTORY": "📋 History",
- "ADMIN_PINNED_MESSAGE": "📌 Pinned message",
- "ADMIN_PINNED_MESSAGE_UPDATE": "✏️ Update",
- "ADMIN_PINNED_POSITION_BEFORE": "⬆️ Send before menu",
- "ADMIN_PINNED_POSITION_AFTER": "⬇️ Send after menu",
"ADMIN_MONITORING": "🔍 Monitoring",
"ADMIN_MONITORING_ALL_LOGS": "📋 All logs",
"ADMIN_MONITORING_AUTOPAY_SETTINGS": "💳 Auto-pay settings",
diff --git a/app/localization/locales/ru.json b/app/localization/locales/ru.json
index a4e0236b..64d4729e 100644
--- a/app/localization/locales/ru.json
+++ b/app/localization/locales/ru.json
@@ -212,10 +212,6 @@
"ADMIN_MESSAGES_BY_CRITERIA": "🔍 По критериям",
"ADMIN_MESSAGES_BY_SUBSCRIPTIONS": "🎯 По подпискам",
"ADMIN_MESSAGES_HISTORY": "📋 История",
- "ADMIN_PINNED_MESSAGE": "📌 Закрепленное сообщение",
- "ADMIN_PINNED_MESSAGE_UPDATE": "✏️ Обновить",
- "ADMIN_PINNED_POSITION_BEFORE": "⬆️ Показать перед меню",
- "ADMIN_PINNED_POSITION_AFTER": "⬇️ Показать после меню",
"ADMIN_MONITORING": "🔍 Мониторинг",
"ADMIN_MONITORING_ALL_LOGS": "📋 Все логи",
"ADMIN_MONITORING_AUTOPAY_SETTINGS": "💳 Настройки автооплаты",
diff --git a/app/localization/locales/ua.json b/app/localization/locales/ua.json
index c2027e0a..dadb5a5e 100644
--- a/app/localization/locales/ua.json
+++ b/app/localization/locales/ua.json
@@ -138,12 +138,8 @@
"ADMIN_MESSAGES_ALL_USERS": "📨 Всім користувачам",
"ADMIN_MESSAGES_BY_CRITERIA": "🔍 За критеріями",
"ADMIN_MESSAGES_BY_SUBSCRIPTIONS": "🎯 За підписками",
- "ADMIN_MESSAGES_HISTORY": "📋 Історія",
- "ADMIN_PINNED_MESSAGE": "📌 Закріплене повідомлення",
- "ADMIN_PINNED_MESSAGE_UPDATE": "✏️ Оновити",
- "ADMIN_PINNED_POSITION_BEFORE": "⬆️ Показати перед меню",
- "ADMIN_PINNED_POSITION_AFTER": "⬇️ Показати після меню",
- "ADMIN_MONITORING": "🔍 Моніторинг",
+ "ADMIN_MESSAGES_HISTORY": "📋 Історія",
+ "ADMIN_MONITORING": "🔍 Моніторинг",
"ADMIN_MONITORING_ALL_LOGS": "📋 Всі логи",
"ADMIN_MONITORING_AUTOPAY_SETTINGS": "💳 Налаштування автооплати",
"ADMIN_MONITORING_AUTO_CLEANUP": "🧹 Автоочищення логів",
diff --git a/app/localization/locales/zh.json b/app/localization/locales/zh.json
index 926e2361..44124ed5 100644
--- a/app/localization/locales/zh.json
+++ b/app/localization/locales/zh.json
@@ -138,10 +138,6 @@
"ADMIN_MESSAGES_BY_CRITERIA":"🔍按条件",
"ADMIN_MESSAGES_BY_SUBSCRIPTIONS":"🎯按订阅",
"ADMIN_MESSAGES_HISTORY":"📋历史记录",
-"ADMIN_PINNED_MESSAGE":"📌置顶消息",
-"ADMIN_PINNED_MESSAGE_UPDATE":"✏️更新",
-"ADMIN_PINNED_POSITION_BEFORE":"⬆️菜单前发送",
-"ADMIN_PINNED_POSITION_AFTER":"⬇️菜单后发送",
"ADMIN_MONITORING":"🔍监控",
"ADMIN_MONITORING_ALL_LOGS":"📋所有日志",
"ADMIN_MONITORING_AUTOPAY_SETTINGS":"💳自动支付设置",
diff --git a/app/services/pinned_message_service.py b/app/services/pinned_message_service.py
deleted file mode 100644
index d534c464..00000000
--- a/app/services/pinned_message_service.py
+++ /dev/null
@@ -1,215 +0,0 @@
-import asyncio
-import logging
-from typing import Optional
-
-from aiogram import Bot
-from aiogram.exceptions import (
- TelegramBadRequest,
- TelegramForbiddenError,
- TelegramRetryAfter,
-)
-from sqlalchemy import select, update
-from sqlalchemy.ext.asyncio import AsyncSession
-
-from app.database.crud.user import get_users_list
-from app.database.models import PinnedMessage, User, UserStatus
-from app.utils.validators import sanitize_html, validate_html_tags
-
-logger = logging.getLogger(__name__)
-
-
-async def get_active_pinned_message(db: AsyncSession) -> Optional[PinnedMessage]:
- result = await db.execute(
- select(PinnedMessage)
- .where(PinnedMessage.is_active.is_(True))
- .order_by(PinnedMessage.created_at.desc())
- .limit(1)
- )
- return result.scalar_one_or_none()
-
-
-async def set_active_pinned_message(
- db: AsyncSession,
- content: str,
- created_by: Optional[int] = None,
- media_type: Optional[str] = None,
- media_file_id: Optional[str] = None,
- send_before_menu: Optional[bool] = None,
-) -> PinnedMessage:
- sanitized_content = sanitize_html(content or "")
- is_valid, error_message = validate_html_tags(sanitized_content)
- if not is_valid:
- raise ValueError(error_message)
-
- if media_type not in {None, "photo", "video"}:
- raise ValueError("Поддерживаются только фото или видео в закрепленном сообщении")
-
- if created_by is not None:
- creator_id = await db.scalar(select(User.id).where(User.id == created_by))
- else:
- creator_id = None
-
- previous_active = await get_active_pinned_message(db)
-
- await db.execute(
- update(PinnedMessage)
- .where(PinnedMessage.is_active.is_(True))
- .values(is_active=False)
- )
-
- pinned_message = PinnedMessage(
- content=sanitized_content,
- media_type=media_type,
- media_file_id=media_file_id,
- is_active=True,
- created_by=creator_id,
- send_before_menu=(
- send_before_menu
- if send_before_menu is not None
- else getattr(previous_active, "send_before_menu", True)
- ),
- )
-
- db.add(pinned_message)
- await db.commit()
- await db.refresh(pinned_message)
-
- logger.info("Создано новое закрепленное сообщение #%s", pinned_message.id)
- return pinned_message
-
-
-async def deliver_pinned_message_to_user(
- bot: Bot,
- db: AsyncSession,
- user: User,
- pinned_message: Optional[PinnedMessage] = None,
-) -> bool:
- pinned_message = pinned_message or await get_active_pinned_message(db)
- if not pinned_message:
- return False
-
- return await _send_and_pin_message(bot, user.telegram_id, pinned_message)
-
-
-async def broadcast_pinned_message(
- bot: Bot,
- db: AsyncSession,
- pinned_message: PinnedMessage,
-) -> tuple[int, int]:
- users: list[User] = []
- offset = 0
- batch_size = 5000
-
- while True:
- batch = await get_users_list(
- db,
- offset=offset,
- limit=batch_size,
- status=UserStatus.ACTIVE,
- )
-
- if not batch:
- break
-
- users.extend(batch)
- offset += batch_size
-
- sent_count = 0
- failed_count = 0
- semaphore = asyncio.Semaphore(3)
-
- async def send_to_user(user: User) -> None:
- nonlocal sent_count, failed_count
- async with semaphore:
- for attempt in range(3):
- try:
- success = await _send_and_pin_message(
- bot,
- user.telegram_id,
- pinned_message,
- )
- if success:
- sent_count += 1
- else:
- failed_count += 1
- break
- except TelegramRetryAfter as retry_error:
- delay = min(retry_error.retry_after + 1, 30)
- logger.warning(
- "RetryAfter for user %s, waiting %s seconds",
- user.telegram_id,
- delay,
- )
- await asyncio.sleep(delay)
- except Exception as send_error: # noqa: BLE001
- logger.error(
- "Ошибка отправки закрепленного сообщения пользователю %s: %s",
- user.telegram_id,
- send_error,
- )
- failed_count += 1
- break
-
- for i in range(0, len(users), 30):
- batch = users[i : i + 30]
- tasks = [send_to_user(user) for user in batch]
- await asyncio.gather(*tasks)
- await asyncio.sleep(0.05)
-
- return sent_count, failed_count
-
-
-async def _send_and_pin_message(bot: Bot, chat_id: int, pinned_message: PinnedMessage) -> bool:
- try:
- await bot.unpin_all_chat_messages(chat_id=chat_id)
- except TelegramBadRequest:
- pass
- except TelegramForbiddenError:
- return False
-
- try:
- if pinned_message.media_type == "photo" and pinned_message.media_file_id:
- sent_message = await bot.send_photo(
- chat_id=chat_id,
- photo=pinned_message.media_file_id,
- caption=pinned_message.content or None,
- parse_mode="HTML" if pinned_message.content else None,
- disable_notification=True,
- )
- elif pinned_message.media_type == "video" and pinned_message.media_file_id:
- sent_message = await bot.send_video(
- chat_id=chat_id,
- video=pinned_message.media_file_id,
- caption=pinned_message.content or None,
- parse_mode="HTML" if pinned_message.content else None,
- disable_notification=True,
- )
- else:
- sent_message = await bot.send_message(
- chat_id=chat_id,
- text=pinned_message.content,
- parse_mode="HTML",
- disable_web_page_preview=True,
- )
- await bot.pin_chat_message(
- chat_id=chat_id,
- message_id=sent_message.message_id,
- disable_notification=True,
- )
- return True
- except TelegramForbiddenError:
- return False
- except TelegramBadRequest as error:
- logger.warning(
- "Некорректный запрос при отправке закрепленного сообщения в чат %s: %s",
- chat_id,
- error,
- )
- except Exception as error: # noqa: BLE001
- logger.error(
- "Не удалось отправить закрепленное сообщение пользователю %s: %s",
- chat_id,
- error,
- )
-
- return False
diff --git a/app/states.py b/app/states.py
index ba8cfb0c..795a6d67 100644
--- a/app/states.py
+++ b/app/states.py
@@ -134,7 +134,6 @@ class AdminStates(StatesGroup):
creating_server_country = State()
editing_welcome_text = State()
- editing_pinned_message = State()
waiting_for_message_buttons = "waiting_for_message_buttons"
editing_promo_offer_message = State()
diff --git a/migrations/alembic/versions/5f2a3e099427_add_media_fields_to_pinned_messages.py b/migrations/alembic/versions/5f2a3e099427_add_media_fields_to_pinned_messages.py
deleted file mode 100644
index fdd05440..00000000
--- a/migrations/alembic/versions/5f2a3e099427_add_media_fields_to_pinned_messages.py
+++ /dev/null
@@ -1,75 +0,0 @@
-"""add media fields to pinned messages"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-revision: str = "5f2a3e099427"
-down_revision: Union[str, None] = "c9c71d04f0a1"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-TABLE_NAME = "pinned_messages"
-
-
-def _table_exists(inspector: sa.Inspector) -> bool:
- return TABLE_NAME in inspector.get_table_names()
-
-
-def _column_missing(inspector: sa.Inspector, column_name: str) -> bool:
- columns = {column.get("name") for column in inspector.get_columns(TABLE_NAME)}
- return column_name not in columns
-
-
-def upgrade() -> None:
- bind = op.get_bind()
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector):
- return
-
- if _column_missing(inspector, "media_type"):
- op.add_column(
- TABLE_NAME,
- sa.Column("media_type", sa.String(length=32), nullable=True),
- )
-
- if _column_missing(inspector, "media_file_id"):
- op.add_column(
- TABLE_NAME,
- sa.Column("media_file_id", sa.String(length=255), nullable=True),
- )
-
- # Ensure content has a default value for media-only messages
- op.alter_column(
- TABLE_NAME,
- "content",
- existing_type=sa.Text(),
- nullable=False,
- server_default="",
- )
-
-
-def downgrade() -> None:
- bind = op.get_bind()
- inspector = sa.inspect(bind)
-
- if not _table_exists(inspector):
- return
-
- if not _column_missing(inspector, "media_type"):
- op.drop_column(TABLE_NAME, "media_type")
-
- if not _column_missing(inspector, "media_file_id"):
- op.drop_column(TABLE_NAME, "media_file_id")
-
- op.alter_column(
- TABLE_NAME,
- "content",
- existing_type=sa.Text(),
- nullable=False,
- server_default=None,
- )
diff --git a/migrations/alembic/versions/7a3c0b8f5b84_add_send_before_menu_to_pinned_messages.py b/migrations/alembic/versions/7a3c0b8f5b84_add_send_before_menu_to_pinned_messages.py
deleted file mode 100644
index 3c92c210..00000000
--- a/migrations/alembic/versions/7a3c0b8f5b84_add_send_before_menu_to_pinned_messages.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""add send_before_menu to pinned messages
-
-Revision ID: 7a3c0b8f5b84
-Revises: 5f2a3e099427
-Create Date: 2025-02-05 00:00:00.000000
-"""
-
-from alembic import op
-import sqlalchemy as sa
-
-
-# revision identifiers, used by Alembic.
-revision = "7a3c0b8f5b84"
-down_revision = "5f2a3e099427"
-branch_labels = None
-depends_on = None
-
-
-def upgrade() -> None:
- op.add_column(
- "pinned_messages",
- sa.Column(
- "send_before_menu",
- sa.Boolean(),
- nullable=False,
- server_default=sa.text("1"),
- ),
- )
-
-
-def downgrade() -> None:
- op.drop_column("pinned_messages", "send_before_menu")
diff --git a/migrations/alembic/versions/c9c71d04f0a1_add_pinned_messages_table.py b/migrations/alembic/versions/c9c71d04f0a1_add_pinned_messages_table.py
deleted file mode 100644
index add5fe11..00000000
--- a/migrations/alembic/versions/c9c71d04f0a1_add_pinned_messages_table.py
+++ /dev/null
@@ -1,45 +0,0 @@
-"""add pinned messages table"""
-
-from typing import Sequence, Union
-
-from alembic import op
-import sqlalchemy as sa
-
-
-revision: str = "c9c71d04f0a1"
-down_revision: Union[str, None] = "e3c1e0b5b4a7"
-branch_labels: Union[str, Sequence[str], None] = None
-depends_on: Union[str, Sequence[str], None] = None
-
-
-TABLE_NAME = "pinned_messages"
-
-
-def _table_exists(inspector: sa.Inspector) -> bool:
- return TABLE_NAME in inspector.get_table_names()
-
-
-def upgrade() -> None:
- bind = op.get_bind()
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector):
- return
-
- op.create_table(
- TABLE_NAME,
- sa.Column("id", sa.Integer(), primary_key=True, index=True),
- sa.Column("content", sa.Text(), nullable=False),
- sa.Column("is_active", sa.Boolean(), default=True),
- sa.Column("created_by", sa.Integer(), sa.ForeignKey("users.id", ondelete="SET NULL"), nullable=True),
- sa.Column("created_at", sa.DateTime(), server_default=sa.func.now()),
- sa.Column("updated_at", sa.DateTime(), server_default=sa.func.now(), onupdate=sa.func.now()),
- )
-
-
-def downgrade() -> None:
- bind = op.get_bind()
- inspector = sa.inspect(bind)
-
- if _table_exists(inspector):
- op.drop_table(TABLE_NAME)