Initial commit
All checks were successful
Build and Run VSTU Vk Poster / build_and_run (push) Successful in 24s

This commit is contained in:
2026-04-04 18:18:05 +03:00
commit 6b72b9d479
5 changed files with 181 additions and 0 deletions

View File

@@ -0,0 +1,52 @@
name: Build and Run VSTU Vk Poster
on:
push:
branches:
- main
jobs:
build_and_run:
runs-on: ubuntu-latest
steps:
# Шаг 1: Получаем исходный код проекта
- name: Checkout code
uses: actions/checkout@v3
# Шаг 2: Сборка Docker-образа локально на хост-машине
# Мы не пушим его в registry, а просто создаем с нужным тегом.
- name: Build Docker image
run: docker build -t vstu_vk_poster:latest .
# Шаг 3: Перезапуск контейнера на хост-машине
# Это сердце упрощенного workflow
- name: Restart the container
run: |
# 1. Останавливаем и удаляем старый контейнер, если он существует.
# `docker ps -q -f name=...` вернет ID контейнера, если он запущен.
# Конструкция `[ $(...) ] && ...` проверит, не пустой ли вывод.
if [ "$(docker ps -q -f name=vstu_vk_poster)" ]; then
echo "Stopping and removing existing container..."
docker stop vstu_vk_poster
docker rm vstu_vk_poster
else
echo "No running container found. Skipping stop/remove."
fi
# 2. Запускаем новый контейнер из только что собранного локального образа.
# Команда точно такая же, как твоя.
echo "Starting new container..."
docker run -d \
--network cl2so4 \
--ip 172.20.0.61 \
-v /home/holder/fclay/secrets/vstu_vk_poster.env:/app/.env \
--restart=always \
--name=vstu_vk_poster \
vstu_vk_poster:latest
# (Опционально) Шаг 4: Очистка старых, "висячих" образов
# Это хорошая практика, чтобы не засорять диск.
- name: Clean up old images
if: always() # Выполнять этот шаг всегда, даже если предыдущие провалились
run: docker image prune -f

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env

6
Dockerfile Normal file
View File

@@ -0,0 +1,6 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "-u", "main.py"]

119
main.py Normal file
View File

@@ -0,0 +1,119 @@
import os
import json
import asyncio
import logging
import aiohttp
import aio_pika
from dotenv import load_dotenv
load_dotenv()
# --- КОНФИГУРАЦИЯ ---
RABBITMQ_URL = os.getenv("RABBITMQ_URL")
EXCHANGE_NAME = os.getenv("OUT_EXCHANGE", "vstu_schedule")
VK_TOKEN = os.getenv("VK_TOKEN")
VK_GROUP_ID = os.getenv("VK_GROUP_ID") # 237378775
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger("VKPublisher")
async def post_to_vk(text: str) -> bool:
"""Возвращает True при успехе, False при временной ошибке"""
url = "https://api.vk.com/method/wall.post"
params = {
"owner_id": f"-{VK_GROUP_ID}",
"from_group": 1,
"message": text,
"access_token": VK_TOKEN,
"v": "5.131"
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(url, data=params, timeout=15) as resp:
result = await resp.json()
if "error" in result:
err_code = result['error']['error_code']
logger.error(f"[!] VK Error {err_code}: {result['error']['error_msg']}")
# Если ошибка в токене или правах (код 5, 15, 100) — ретрай бесполезен
if err_code in [5, 15, 100]:
return True # Условно "успех", чтобы не зацикливать очередь
return False # Временная ошибка (лимиты, сервер)
return True
except Exception as e:
logger.error(f"Network error in post_to_vk: {e}")
return False
async def process_message(message: aio_pika.IncomingMessage):
"""Парсит событие от SLS с гарантией доставки"""
try:
# Мы НЕ используем 'async with message.process()', чтобы контролировать ack/nack вручную
payload = json.loads(message.body.decode())
# 1. Подготовка текста (Логика данных)
shm = payload.get("humanlike_message", "")
if not shm:
logger.warning("Пустое сообщение. Удаляем из очереди.")
await message.ack() # Подтверждаем, чтобы не висело, раз оно пустое
return
facultet = payload.get("facultet", "news").upper()
vk_text = f"#{facultet}@vstu_rasp\n\n{shm}"
ai = payload.get("ai_summary")
if ai and isinstance(ai, dict) and 'wide_review' in ai:
vk_text += f"\n\n🤖 AI Резюме: {ai['wide_review']}"
vk_text += "\n\n🔗 fazziclay.com"
# 2. Попытка публикации (Логика сети)
success = await post_to_vk(vk_text)
if success:
# ТОЛЬКО ТЕПЕРЬ удаляем из очереди
await message.ack()
logger.info(f"[v] Сообщение успешно обработано и подтверждено.")
else:
# VK API недоступен или лимиты — возвращаем в очередь
logger.warning(f"[!] Ошибка публикации. Возврат сообщения в очередь для повтора...")
await asyncio.sleep(10) # Небольшая пауза, чтобы не спамить ретраями мгновенно
await message.nack(requeue=True)
except json.JSONDecodeError:
logger.error("Критическая ошибка: битый JSON. Удаление сообщения.")
await message.ack() # Ack, потому что ретрай не поможет распарсить плохой JSON
except Exception as e:
logger.error(f"Непредвиденная ошибка в воркере: {e}")
# В случае неизвестной ошибки лучше вернуть в очередь (requeue)
await message.nack(requeue=True)
async def main():
if not VK_TOKEN:
logger.error("VK_TOKEN не найден в .env!")
return
connection = await aio_pika.connect_robust(RABBITMQ_URL)
channel = await connection.channel()
# Объявляем тот же обменник, в который пишет SLS
exchange = await channel.declare_exchange(EXCHANGE_NAME, aio_pika.ExchangeType.TOPIC, durable=True)
# Создаем свою очередь для VK (чтобы сообщения не пересекались с телеграмом)
queue = await channel.declare_queue("vk_publisher_queue", durable=True)
# Подписываемся на события изменения файлов
# Маска: schedule_logging_service.event.excel.#
await queue.bind(exchange, routing_key="schedule_logging_service.event.excel.#")
logger.info(f"[*] VK Publisher запущен. Ожидание событий из {EXCHANGE_NAME}...")
await queue.consume(process_message)
try:
await asyncio.Future() # Вечный цикл
finally:
await connection.close()
if __name__ == "__main__":
asyncio.run(main())

3
requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
aio_pika==9.6.2
aiohttp
python-dotenv