From 94a12fc853cfc97d0d36a6c2e89b48b6e2dbc3e8 Mon Sep 17 00:00:00 2001 From: Mark Arzangulyan <15670678+Arzangulyan@users.noreply.github.com> Date: Fri, 7 Mar 2025 22:32:34 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=D0=B0=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=D1=8B=20=D0=92?= =?UTF-8?q?=D0=9A=20=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D1=81=D1=8B=D0=BB?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=BF=D0=BE=D1=81=D1=82=D0=BE=D0=B2=20=D0=B2=20?= =?UTF-8?q?Telegram-=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- social/handlers_vk/base.py | 162 +++++++++++++++++++++++++++++++++++++ social/routes/vk.py | 105 +++++++++++++++++++----- social/settings.py | 2 + 3 files changed, 247 insertions(+), 22 deletions(-) diff --git a/social/handlers_vk/base.py b/social/handlers_vk/base.py index b8fbeca..b218aa5 100644 --- a/social/handlers_vk/base.py +++ b/social/handlers_vk/base.py @@ -1,12 +1,18 @@ import logging from collections.abc import Callable +import json +import re +import requests from social.utils.events import EventProcessor from social.utils.vk_groups import approve_vk_chat +from social.settings import get_settings +from telegram import Bot, InputMediaPhoto logger = logging.getLogger(__name__) EVENT_PROCESSORS: list[EventProcessor] = [] +settings = get_settings() def event(**filters: str): @@ -34,3 +40,159 @@ def process_event(event: dict): def validate_group(event: dict): """Если получено сообщение команды /validate, то за группой закрепляется владелец""" approve_vk_chat(event) + + +async def send_to_telegram(message: str, photos: list = None): + """Отправляет сообщение и фотографии в Telegram канал""" + if not settings.TELEGRAM_BOT_TOKEN or not settings.TELEGRAM_TARGET_CHANNEL_ID: + logger.warning("Telegram bot token or channel ID not configured") + return + + bot = Bot(token=settings.TELEGRAM_BOT_TOKEN) + + try: + if not photos: + # Если нет фотографий, отправляем только текст + await bot.send_message( + chat_id=settings.TELEGRAM_TARGET_CHANNEL_ID, + text=message, + parse_mode='HTML', + disable_web_page_preview=False + ) + elif len(photos) == 1: + # Если только одна фотография, отправляем ее с подписью + await bot.send_photo( + chat_id=settings.TELEGRAM_TARGET_CHANNEL_ID, + photo=photos[0], + caption=message, + parse_mode='HTML' + ) + else: + # Если несколько фотографий, отправляем их как медиагруппу + media_group = [] + + # Первая фотография с подписью (текстом сообщения) + media_group.append(InputMediaPhoto( + media=photos[0], + caption=message, + parse_mode='HTML' + )) + + # Все остальные фотографии без подписи + for photo_url in photos[1:]: + media_group.append(InputMediaPhoto( + media=photo_url + )) + + await bot.send_media_group( + chat_id=settings.TELEGRAM_TARGET_CHANNEL_ID, + media=media_group + ) + + logger.info(f"Message successfully sent to Telegram channel {settings.TELEGRAM_TARGET_CHANNEL_ID}") + except Exception as e: + logger.error(f"Failed to send message to Telegram: {e}") + + +@event( + type="wall_post_new", + group_id=lambda i: int(i) == settings.VK_MONITORED_GROUP_ID if settings.VK_MONITORED_GROUP_ID else False, +) +def handle_new_post(event: dict): + """Обрабатывает событие нового поста в группе ВК и пересылает его в Telegram канал""" + logger.info("New post detected in monitored VK group") + + try: + # Получаем данные поста + post = event.get("object", {}) + post_id = post.get("id") + owner_id = post.get("owner_id") + text = post.get("text", "") + attachments = post.get("attachments", []) + + # Форматируем сообщение для Telegram + message = f"Новый пост в группе ВК:\n\n{text}" + + # Добавляем информацию о вложениях, кроме фото (их отправим отдельно) + attachment_texts = [] + + for attachment in attachments: + attachment_type = attachment.get("type") + + # Обрабатываем видео + if attachment_type == "video": + video_data = attachment.get("video", {}) + video_id = video_data.get("id") + video_owner_id = video_data.get("owner_id") + video_title = video_data.get("title", "Видео") + + if video_id and video_owner_id: + attachment_texts.append( + f"\n\n📹 {video_title}: " + f"Смотреть видео" + ) + + # Обрабатываем ссылки + elif attachment_type == "link": + link_data = attachment.get("link", {}) + link_url = link_data.get("url") + link_title = link_data.get("title", "Ссылка") + + if link_url: + attachment_texts.append( + f"\n\n🔗 {link_title}: Открыть ссылку" + ) + + # Обрабатываем документы + elif attachment_type == "doc": + doc_data = attachment.get("doc", {}) + doc_url = doc_data.get("url") + doc_title = doc_data.get("title", "Документ") + + if doc_url: + attachment_texts.append( + f"\n\n📄 {doc_title}: Скачать документ" + ) + + # Обрабатываем аудио + elif attachment_type == "audio": + audio_data = attachment.get("audio", {}) + audio_id = audio_data.get("id") + audio_owner_id = audio_data.get("owner_id") + audio_artist = audio_data.get("artist", "") + audio_title = audio_data.get("title", "Аудиозапись") + + if audio_id and audio_owner_id: + attachment_texts.append( + f"\n\n🎵 {audio_artist} - {audio_title}: " + f"Слушать" + ) + + # Добавляем информацию о вложениях к сообщению + if attachment_texts: + message += "".join(attachment_texts) + + # Добавляем ссылку на оригинальный пост в конце + message += f"\n\nОригинальный пост ВКонтакте" + + # Собираем фотографии из вложений + photos = [] + for attachment in attachments: + if attachment.get("type") == "photo": + photo_data = attachment.get("photo", {}) + # Выбираем максимальное разрешение фото + sizes = photo_data.get("sizes", []) + if sizes: + # Сортируем по размеру (width * height) + sizes.sort(key=lambda x: x.get("width", 0) * x.get("height", 0), reverse=True) + photo_url = sizes[0].get("url") + if photo_url: + photos.append(photo_url) + + # Отправляем в Telegram + import asyncio + asyncio.run(send_to_telegram(message, photos)) + + logger.info(f"Post content forwarded to Telegram channel from VK post {owner_id}_{post_id}") + except Exception as e: + logger.exception(f"Error processing new VK post: {e}") diff --git a/social/routes/vk.py b/social/routes/vk.py index f44b361..b0a0637 100644 --- a/social/routes/vk.py +++ b/social/routes/vk.py @@ -31,32 +31,65 @@ class VkGroupCreateResponse(BaseModel): model_config = ConfigDict(from_attributes=True) +class MonitoringConfig(BaseModel): + vk_group_id: int + telegram_channel_id: int + + @router.post('', tags=["webhooks"]) async def vk_webhook(request: Request, background_tasks: BackgroundTasks) -> str: """Принимает любой POST запрос от VK""" - request_data = await request.json() - logger.debug(request_data) - group_id = request_data["group_id"] # Fail if no group - group = db.session.query(VkGroup).where(VkGroup.group_id == group_id).one() # Fail if no settings - - # Проверка на создание нового вебхука со страничка ВК - if request_data.get("type", "") == "confirmation": - return PlainTextResponse(group.confirmation_token) - - if request_data.get("secret") != group.secret_key: - raise Exception("Not a valid secret") - - db.session.add( - WebhookStorage( - system=WebhookSystems.VK, - message=request_data, + try: + request_data = await request.json() + logger.debug(f"Received VK webhook: {request_data}") + group_id = request_data.get("group_id") # Получаем ID группы + + if not group_id: + logger.warning("Received VK webhook without group_id") + return PlainTextResponse('ok') # Возвращаем ok, чтобы VK не пытался повторить запрос + + # Проверяем наличие группы в базе данных + group = db.session.query(VkGroup).where(VkGroup.group_id == group_id).one_or_none() + + if not group: + logger.warning(f"Received VK webhook for unknown group_id: {group_id}") + return PlainTextResponse('ok') + + # Проверка на создание нового вебхука со страницы ВК + if request_data.get("type", "") == "confirmation": + logger.info(f"Received confirmation request for group_id: {group_id}") + return PlainTextResponse(group.confirmation_token) + + # Проверка секретного ключа + if request_data.get("secret") != group.secret_key: + logger.warning(f"Received VK webhook with invalid secret for group_id: {group_id}") + return PlainTextResponse('ok') # Возвращаем ok, но не обрабатываем + + event_type = request_data.get("type", "unknown") + logger.info(f"Processing VK webhook, type: {event_type}, group_id: {group_id}") + + # Сохраняем событие в базу данных + db.session.add( + WebhookStorage( + system=WebhookSystems.VK, + message=request_data, + ) ) - ) - db.session.commit() - - background_tasks.add_task(create_vk_chat, request_data) - background_tasks.add_task(process_event, request_data) - return PlainTextResponse('ok') + db.session.commit() + + # Проверяем, это ли событие создания записи на стене + is_wall_post = event_type == "wall_post_new" + if is_wall_post: + logger.info(f"Received new wall post event for group_id: {group_id}") + + # Запускаем обработку в фоновом режиме + background_tasks.add_task(create_vk_chat, request_data) + background_tasks.add_task(process_event, request_data) + + return PlainTextResponse('ok') + except Exception as e: + logger.exception(f"Error processing VK webhook: {e}") + return PlainTextResponse('ok') # Всегда возвращаем ok, чтобы VK не повторял запрос @router.put('/{group_id}') @@ -80,3 +113,31 @@ def create_or_replace_group( db.session.commit() return group + + +@router.post('/monitoring/configure') +def configure_monitoring( + config: MonitoringConfig, + user: dict[str] = Depends(UnionAuth(["social.monitoring.configure"])), +) -> dict: + """Настраивает мониторинг группы ВК и пересылку постов в Telegram канал""" + # Здесь мы обновляем настройки приложения + # В реальном приложении нужно будет сохранять эти настройки в базу данных + # и загружать их при старте, а не менять глобальный объект + + settings = get_settings() + + # В данном примере мы напрямую изменяем настройки + # Но лучше будет сохранить их в базу и обновлять при перезапуске + # Для этого потребуется создать соответствующую модель БД + settings.VK_MONITORED_GROUP_ID = config.vk_group_id + settings.TELEGRAM_TARGET_CHANNEL_ID = config.telegram_channel_id + + logger.info(f"Monitoring configured for VK group {config.vk_group_id} with Telegram channel {config.telegram_channel_id}") + + return { + "status": "success", + "message": "Мониторинг настроен успешно", + "vk_group_id": config.vk_group_id, + "telegram_channel_id": config.telegram_channel_id + } diff --git a/social/settings.py b/social/settings.py index 83fbf6b..0b8bdda 100644 --- a/social/settings.py +++ b/social/settings.py @@ -19,9 +19,11 @@ class Settings(BaseSettings): CORS_ALLOW_HEADERS: list[str] = ['*'] TELEGRAM_BOT_TOKEN: str | None = None + TELEGRAM_TARGET_CHANNEL_ID: int | None = None # ID канала Telegram для пересылки постов VK_BOT_GROUP_ID: int | None = None VK_BOT_TOKEN: str | None = None + VK_MONITORED_GROUP_ID: int | None = None # ID группы ВК для мониторинга GITHUB_APP_ID: int | None = None GITHUB_WEBHOOK_SECRET: str | None = None