102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
import os
|
|
import asyncio
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
from typing import Optional
|
|
from telethon import TelegramClient
|
|
from db.repository import Repository
|
|
from core.metrics import POSTS_PUBLISHED_TOTAL, QUEUE_POSTS_GAUGE
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SESSION_DIR = os.getenv("SESSION_DIR", "/app/sessions" if os.path.exists("/app") else "/projects/telegram-bots/copykar/sessions")
|
|
|
|
class PublisherService:
|
|
def __init__(
|
|
self,
|
|
repo: Repository,
|
|
api_id: Optional[int] = None,
|
|
api_hash: Optional[str] = None,
|
|
session_name: Optional[str] = None,
|
|
bot_token: Optional[str] = None,
|
|
):
|
|
self.repo = repo
|
|
self.api_id = api_id or int(os.getenv("API_ID", "0"))
|
|
self.api_hash = api_hash or os.getenv("API_HASH", "")
|
|
self.bot_token = bot_token or os.getenv("BOT_TOKEN")
|
|
self.session_name = session_name or os.path.join(SESSION_DIR, "publisher.session")
|
|
os.makedirs(os.path.dirname(self.session_name), exist_ok=True)
|
|
self.client = TelegramClient(self.session_name, self.api_id, self.api_hash)
|
|
self._running = False
|
|
self._task: Optional[asyncio.Task] = None
|
|
|
|
async def start(self):
|
|
logger.info("Starting Publisher Service...")
|
|
if self.bot_token:
|
|
await self.client.start(bot_token=self.bot_token)
|
|
else:
|
|
await self.client.start()
|
|
logger.info("Publisher Service connected successfully.")
|
|
|
|
self._running = True
|
|
self._task = asyncio.create_task(self._publisher_loop())
|
|
|
|
async def _publisher_loop(self):
|
|
while self._running:
|
|
try:
|
|
await self._process_pending_queues()
|
|
except Exception as e:
|
|
logger.error(f"Error in publisher loop: {e}", exc_info=True)
|
|
await asyncio.sleep(15)
|
|
|
|
async def _process_pending_queues(self):
|
|
targets = await self.repo.get_active_targets()
|
|
now = datetime.now(timezone.utc)
|
|
|
|
pending_count = len(await self.repo.get_posts_by_status("approved", limit=5000))
|
|
QUEUE_POSTS_GAUGE.labels(status="approved").set(pending_count)
|
|
|
|
for target in targets:
|
|
if target.last_post_time:
|
|
last_post = target.last_post_time
|
|
if last_post.tzinfo is None:
|
|
last_post = last_post.replace(tzinfo=timezone.utc)
|
|
diff_minutes = (now - last_post).total_seconds() / 60.0
|
|
if diff_minutes < target.post_interval_min:
|
|
continue
|
|
|
|
post = await self.repo.get_next_approved_post_for_target(target.id)
|
|
if not post:
|
|
continue
|
|
|
|
try:
|
|
publish_text = post.ai_text or post.raw_text or ""
|
|
if post.media_path and os.path.exists(post.media_path):
|
|
await self.client.send_file(
|
|
target.channel_id,
|
|
file=post.media_path,
|
|
caption=publish_text,
|
|
parse_mode="markdown"
|
|
)
|
|
else:
|
|
await self.client.send_message(
|
|
target.channel_id,
|
|
publish_text,
|
|
parse_mode="markdown"
|
|
)
|
|
|
|
await self.repo.mark_post_published(post.id)
|
|
await self.repo.update_target_last_post(target.id)
|
|
POSTS_PUBLISHED_TOTAL.labels(target_channel_id=str(target.channel_id)).inc()
|
|
logger.info(f"Successfully published post {post.id} to target channel {target.title} ({target.channel_id})")
|
|
except Exception as e:
|
|
logger.error(f"Failed to publish post {post.id} to target {target.channel_id}: {e}", exc_info=True)
|
|
|
|
async def stop(self):
|
|
self._running = False
|
|
if self._task:
|
|
self._task.cancel()
|
|
if self.client.is_connected():
|
|
await self.client.disconnect()
|
|
logger.info("Publisher Service disconnected.")
|