Initial commit
All checks were successful
Build and Run VSTU Vk Poster / build_and_run (push) Successful in 24s
All checks were successful
Build and Run VSTU Vk Poster / build_and_run (push) Successful in 24s
This commit is contained in:
52
.gitea/workflows/deploy.yml
Normal file
52
.gitea/workflows/deploy.yml
Normal 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
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.env
|
||||
6
Dockerfile
Normal file
6
Dockerfile
Normal 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
119
main.py
Normal 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
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
aio_pika==9.6.2
|
||||
aiohttp
|
||||
python-dotenv
|
||||
Reference in New Issue
Block a user