diff --git a/database/migrate_v3_4_documents.py b/database/migrate_v3_4_documents.py new file mode 100644 index 0000000..455d885 --- /dev/null +++ b/database/migrate_v3_4_documents.py @@ -0,0 +1,36 @@ +""" +Миграция V3.4 - Добавление поддержки документов в запланированные посты +""" +import asyncio +import aiosqlite +import os +from pathlib import Path + +DB_PATH = Path(__file__).parent.parent / "database" / "domovoy.db" + +async def migrate(): + print("🔄 Миграция V3.4: Добавление поддержки документов...") + + async with aiosqlite.connect(str(DB_PATH)) as db: + # Проверяем существуют ли уже колонки + cursor = await db.execute("PRAGMA table_info(scheduled_posts)") + columns = [row[1] for row in await cursor.fetchall()] + + if 'document_file_id' not in columns: + await db.execute("ALTER TABLE scheduled_posts ADD COLUMN document_file_id VARCHAR(255)") + print("✅ Добавлена колонка: document_file_id") + else: + print("⏭️ document_file_id уже существует") + + if 'document_name' not in columns: + await db.execute("ALTER TABLE scheduled_posts ADD COLUMN document_name VARCHAR(255)") + print("✅ Добавлена колонка: document_name") + else: + print("⏭️ document_name уже существует") + + await db.commit() + + print("✅ Миграция V3.4 завершена!") + +if __name__ == "__main__": + asyncio.run(migrate()) diff --git a/database/models.py b/database/models.py index 9285499..2d16ceb 100644 --- a/database/models.py +++ b/database/models.py @@ -604,6 +604,8 @@ class ScheduledPost(Base): id = Column(Integer, primary_key=True, autoincrement=True) text = Column(Text, nullable=False) # Текст поста photo_file_id = Column(String(255), nullable=True) # ID фото в Telegram + document_file_id = Column(String(255), nullable=True) # ID документа (PDF, DOCX...) + document_name = Column(String(255), nullable=True) # Имя файла документа topic_id = Column(BigInteger, nullable=True) # ID темы форума (message_thread_id) topic_name = Column(String(50), nullable=True) # Название темы ('general', 'memes'...) recipients = Column(String(50), default='chat_only') # Получатели: 'chat_only', 'all_verified', 'all_and_chat' diff --git a/services/scheduler.py b/services/scheduler.py index 3b2d477..af3fc28 100644 --- a/services/scheduler.py +++ b/services/scheduler.py @@ -358,7 +358,27 @@ class Scheduler: # HTTP прокси connector = aiohttp.TCPConnector() - if post.photo_file_id: + if post.document_file_id: + # С документом - отправляем по file_id + url = f"{base_url}/sendDocument" + params = { + 'chat_id': ADMIN_CHAT_ID, + 'document': post.document_file_id, + 'caption': post.text, + 'parse_mode': 'HTML' + } + if post.topic_id and post.topic_id > 1: + params['message_thread_id'] = post.topic_id + + async with aiohttp.ClientSession(connector=connector) as http_session: + async with http_session.post(url, json=params, timeout=30) as resp: + if resp.status == 200: + result_data = await resp.json() + message_id = result_data['result']['message_id'] + else: + error_text = await resp.text() + raise Exception(f"Telegram API {resp.status}: {error_text}") + elif post.photo_file_id: # С фото - отправляем по file_id url = f"{base_url}/sendPhoto" params = { @@ -408,7 +428,15 @@ class Scheduler: for user in users: try: - if post.photo_file_id: + if post.document_file_id: + url = f"{base_url}/sendDocument" + params = { + 'chat_id': user.user_id, + 'document': post.document_file_id, + 'caption': f"📢 Объявление от администрации\n\n{post.text}", + 'parse_mode': 'HTML' + } + elif post.photo_file_id: url = f"{base_url}/sendPhoto" params = { 'chat_id': user.user_id, diff --git a/web/app.py b/web/app.py index abfc74a..8ab7595 100644 --- a/web/app.py +++ b/web/app.py @@ -774,11 +774,12 @@ async def api_create_scheduled_post( recipients: str = Form("chat_only"), scheduled_time: str = Form(...), photo: UploadFile = File(None), + document: UploadFile = File(None), # Новый параметр — документ username: str = Depends(get_current_admin) ): - """Создать запланированный пост""" + """Создать запланированный пост (с фото ИЛИ документом)""" from datetime import datetime - from config import ADMIN_CHAT_ID, get_topic_id + from config import ADMIN_CHAT_ID, get_topic_id, get_proxy_url import aiohttp logger.info(f"📅 СОЗДАНИЕ ЗАПЛАНИРОВАННОГО ПОСТА: тема={topic_name}, время={scheduled_time}") @@ -819,20 +820,68 @@ async def api_create_scheduled_post( data.add_field('photo', open(temp_path, 'rb'), filename=photo.filename) data.add_field('caption', 'Preview') - async with session.post(url, data=data) as resp: + async with session.post(url, data=data, proxy=get_proxy_url()) as resp: result = await resp.json() if result.get('ok'): photo_file_id = result['result']['photo'][-1]['file_id'] except Exception as e: logger.error(f"Ошибка загрузки фото: {e}") - # Не блокируем из-за фото photo_file_id = None + # Если есть документ - загружаем в Telegram + document_file_id = None + document_name = None + if document and document.filename: + # Проверяем расширение + allowed_ext = ['.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx', '.txt', '.zip', '.rar'] + file_ext = Path(document.filename).suffix.lower() + if file_ext not in allowed_ext: + return JSONResponse({ + "success": False, + "error": f"Неподдерживаемый формат файла. Разрешены: {', '.join(allowed_ext)}" + }, status_code=400) + + # Сохраняем временно + temp_path = Path("data") / f"scheduled_doc_{document.filename}" + with open(temp_path, "wb") as f: + content = await document.read() + f.write(content) + + try: + async with aiohttp.ClientSession() as session: + url = f"https://api.telegram.org/bot{config.BOT_TOKEN}/sendDocument" + data = aiohttp.FormData() + data.add_field('chat_id', config.ADMIN_USER_ID) + + with open(temp_path, 'rb') as doc_file: + data.add_field('document', doc_file, filename=document.filename) + data.add_field('caption', 'Document preview') + + async with session.post(url, data=data, proxy=get_proxy_url()) as resp: + result = await resp.json() + if result.get('ok'): + document_file_id = result['result']['document']['file_id'] + document_name = document.filename + logger.info(f"✅ Документ загружен: {document_name}, file_id={document_file_id}") + else: + error_msg = result.get('description', 'Неизвестная ошибка') + logger.error(f"Ошибка загрузки документа: {error_msg}") + except Exception as e: + logger.error(f"Ошибка загрузки документа: {e}") + document_file_id = None + document_name = None + + # Удаляем временный файл + if temp_path.exists(): + temp_path.unlink() + # Сохраняем в БД async with AsyncSessionLocal() as session: scheduled_post = ScheduledPost( text=text, photo_file_id=photo_file_id, + document_file_id=document_file_id, + document_name=document_name, topic_id=topic_id, topic_name=topic_name, recipients=recipients, @@ -846,9 +895,15 @@ async def api_create_scheduled_post( logger.info(f"✅ Пост создан: id={scheduled_post.id}") + attachment_info = "" + if document_name: + attachment_info = f" + 📄 {document_name}" + elif photo_file_id: + attachment_info = " + 📷 фото" + return JSONResponse({ "success": True, - "message": f"Пост запланирован на {scheduled_dt.strftime('%d.%m.%Y %H:%M')}", + "message": f"Пост запланирован на {scheduled_dt.strftime('%d.%m.%Y %H:%M')}{attachment_info}", "post_id": scheduled_post.id }) diff --git a/web/templates/scheduled_posts.html b/web/templates/scheduled_posts.html index 6ab3aa4..e231a71 100644 --- a/web/templates/scheduled_posts.html +++ b/web/templates/scheduled_posts.html @@ -38,6 +38,17 @@ +