-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Пересылка постов из ВК в ТГ #37
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Изначально предполагалось, что такие штуки прямо в коде будешь задавать. Не нужно их в настройки вносить По факту эти |
||
) | ||
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"<b>Новый пост в группе ВК:</b>\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<b>📹 {video_title}</b>: " | ||
f"<a href='https://vk.com/video{video_owner_id}_{video_id}'>Смотреть видео</a>" | ||
) | ||
|
||
# Обрабатываем ссылки | ||
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<b>🔗 {link_title}</b>: <a href='{link_url}'>Открыть ссылку</a>" | ||
) | ||
|
||
# Обрабатываем документы | ||
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<b>📄 {doc_title}</b>: <a href='{doc_url}'>Скачать документ</a>" | ||
) | ||
|
||
# Обрабатываем аудио | ||
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<b>🎵 {audio_artist} - {audio_title}</b>: " | ||
f"<a href='https://vk.com/audio{audio_owner_id}_{audio_id}'>Слушать</a>" | ||
) | ||
|
||
Comment on lines
+119
to
+170
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Очень большая вложенность. +Проверить это не могу (нужно читать документацию ВК) По первому пункту: лучше создать файл |
||
# Добавляем информацию о вложениях к сообщению | ||
if attachment_texts: | ||
message += "".join(attachment_texts) | ||
|
||
# Добавляем ссылку на оригинальный пост в конце | ||
message += f"\n\n<a href='https://vk.com/wall{owner_id}_{post_id}'>Оригинальный пост ВКонтакте</a>" | ||
|
||
# Собираем фотографии из вложений | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Импорты лучше делать в начале файла |
||
asyncio.run(send_to_telegram(message, photos)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Вызывать асинхронный код отсюда не надо точно |
||
|
||
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}") | ||
Comment on lines
+197
to
+198
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Внутри обработчика событий и так должен быть большой try/except. Не нужно делать ещё один |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Внутри router.post и так есть обработчик исключений, тут не нужен |
||
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 не повторял запрос | ||
Comment on lines
+78
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Это не нужно. Для этого есть обработчик событий через |
||
|
||
|
||
@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 | ||
} | ||
Comment on lines
+118
to
+143
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Так это та же ручка, что уже есть..... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Этот код я бы унес в утилиты работы с телеграммом. Возможно, такой файл уже есть. Куда-нибудь в utils/telegram.py
И задавал бы Telegram channel id через параметр, а не через настройку. Из настроек убрал бы вообще это параметр. Просто захардкодить внутри
@event
функции