Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 162 additions & 0 deletions social/handlers_vk/base.py
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):
Expand Down Expand Up @@ -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}")
Comment on lines +45 to +94
Copy link
Member

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 функции



@event(
type="wall_post_new",
group_id=lambda i: int(i) == settings.VK_MONITORED_GROUP_ID if settings.VK_MONITORED_GROUP_ID else False,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Изначально предполагалось, что такие штуки прямо в коде будешь задавать. Не нужно их в настройки вносить

По факту эти @event и есть конфиг, хоть и с кодом

)
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Очень большая вложенность. +Проверить это не могу (нужно читать документацию ВК)

По первому пункту: лучше создать файл utils/vk.py и вынести туда отдельными функциями для каждого типа сложений + общую функцию для обработки одного типа сложений. Тогда тут будет for a in attachments: обработай вложение

# Добавляем информацию о вложениях к сообщению
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Импорты лучше делать в начале файла

asyncio.run(send_to_telegram(message, photos))
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Внутри обработчика событий и так должен быть большой try/except. Не нужно делать ещё один

105 changes: 83 additions & 22 deletions social/routes/vk.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Это не нужно. Для этого есть обработчик событий через @event



@router.put('/{group_id}')
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Так это та же ручка, что уже есть.....

2 changes: 2 additions & 0 deletions social/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading