212 lines
8.5 KiB
Python
212 lines
8.5 KiB
Python
import json
|
|
import asyncpg
|
|
from typing import List, Optional, Dict, Any
|
|
from db.models import SourceChannel, TargetChannel, Post, Setting
|
|
from db.database import get_db_pool
|
|
|
|
def _parse_post_row(row: asyncpg.Record) -> Post:
|
|
data = dict(row)
|
|
if isinstance(data.get("published_to"), str):
|
|
try:
|
|
data["published_to"] = json.loads(data["published_to"])
|
|
except Exception:
|
|
data["published_to"] = []
|
|
elif data.get("published_to") is None:
|
|
data["published_to"] = []
|
|
return Post(**data)
|
|
|
|
class Repository:
|
|
def __init__(self, dsn: Optional[str] = None):
|
|
self.dsn = dsn
|
|
|
|
async def _get_pool(self) -> asyncpg.Pool:
|
|
if self.dsn:
|
|
return await get_db_pool(self.dsn)
|
|
return await get_db_pool()
|
|
|
|
# --- Source Channels ---
|
|
async def add_source(self, channel_id: int, title: Optional[str] = None, username: Optional[str] = None) -> int:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
INSERT INTO sources (channel_id, title, username)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT(channel_id) DO UPDATE SET
|
|
title = EXCLUDED.title,
|
|
username = EXCLUDED.username,
|
|
is_active = TRUE
|
|
RETURNING id;
|
|
""",
|
|
channel_id, title, username,
|
|
)
|
|
return row["id"]
|
|
|
|
async def get_active_sources(self) -> List[SourceChannel]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch("SELECT * FROM sources WHERE is_active = TRUE ORDER BY id ASC;")
|
|
return [SourceChannel(**dict(r)) for r in rows]
|
|
|
|
async def get_source_by_channel_id(self, channel_id: int) -> Optional[SourceChannel]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow("SELECT * FROM sources WHERE channel_id = $1;", channel_id)
|
|
return SourceChannel(**dict(row)) if row else None
|
|
|
|
# --- Target Channels ---
|
|
async def add_target(
|
|
self,
|
|
channel_id: int,
|
|
title: Optional[str] = None,
|
|
username: Optional[str] = None,
|
|
post_interval_min: int = 30,
|
|
personality: str = "",
|
|
custom_footer: str = ""
|
|
) -> int:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
INSERT INTO targets (channel_id, title, username, post_interval_min, personality, custom_footer)
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
ON CONFLICT(channel_id) DO UPDATE SET
|
|
title = EXCLUDED.title,
|
|
username = EXCLUDED.username,
|
|
post_interval_min = EXCLUDED.post_interval_min,
|
|
is_active = TRUE
|
|
RETURNING id;
|
|
""",
|
|
channel_id, title, username, post_interval_min, personality, custom_footer,
|
|
)
|
|
return row["id"]
|
|
|
|
async def update_target_personality(self, target_id: int, personality: str, custom_footer: Optional[str] = None) -> None:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
if custom_footer is not None:
|
|
await conn.execute(
|
|
"UPDATE targets SET personality = $1, custom_footer = $2 WHERE id = $3;",
|
|
personality, custom_footer, target_id
|
|
)
|
|
else:
|
|
await conn.execute(
|
|
"UPDATE targets SET personality = $1 WHERE id = $2;",
|
|
personality, target_id
|
|
)
|
|
|
|
async def update_target_footer(self, target_id: int, custom_footer: str) -> None:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
await conn.execute(
|
|
"UPDATE targets SET custom_footer = $1 WHERE id = $2;",
|
|
custom_footer, target_id
|
|
)
|
|
|
|
async def get_active_targets(self) -> List[TargetChannel]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch("SELECT * FROM targets WHERE is_active = TRUE ORDER BY id ASC;")
|
|
return [TargetChannel(**dict(r)) for r in rows]
|
|
|
|
async def get_target_by_id(self, target_id: int) -> Optional[TargetChannel]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow("SELECT * FROM targets WHERE id = $1;", target_id)
|
|
return TargetChannel(**dict(row)) if row else None
|
|
|
|
async def update_target_last_post(self, target_id: int) -> None:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
await conn.execute(
|
|
"UPDATE targets SET last_post_time = CURRENT_TIMESTAMP WHERE id = $1;",
|
|
target_id,
|
|
)
|
|
|
|
# --- Posts & Multi-Channel Dispatch ---
|
|
async def create_raw_post(
|
|
self,
|
|
source_channel_id: int,
|
|
source_message_id: int,
|
|
raw_text: Optional[str],
|
|
media_path: Optional[str] = None,
|
|
media_type: Optional[str] = None,
|
|
content_hash: Optional[str] = None,
|
|
is_duplicate: bool = False,
|
|
duplicate_of_id: Optional[int] = None,
|
|
similarity_reason: Optional[str] = None,
|
|
) -> Optional[int]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
try:
|
|
row = await conn.fetchrow(
|
|
"""
|
|
INSERT INTO posts (
|
|
source_channel_id, source_message_id, raw_text, media_path,
|
|
media_type, content_hash, is_duplicate, duplicate_of_id, similarity_reason, status
|
|
)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, 'pending_review')
|
|
RETURNING id;
|
|
""",
|
|
source_channel_id,
|
|
source_message_id,
|
|
raw_text,
|
|
media_path,
|
|
media_type,
|
|
content_hash,
|
|
is_duplicate,
|
|
duplicate_of_id,
|
|
similarity_reason,
|
|
)
|
|
return row["id"] if row else None
|
|
except asyncpg.UniqueViolationError:
|
|
return None
|
|
|
|
async def get_post_by_id(self, post_id: int) -> Optional[Post]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
row = await conn.fetchrow("SELECT * FROM posts WHERE id = $1;", post_id)
|
|
return _parse_post_row(row) if row else None
|
|
|
|
async def get_posts_by_status(self, status: str, limit: int = 20) -> List[Post]:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
rows = await conn.fetch(
|
|
"SELECT * FROM posts WHERE status = $1 ORDER BY id ASC LIMIT $2;",
|
|
status, limit,
|
|
)
|
|
return [_parse_post_row(r) for r in rows]
|
|
|
|
async def update_review_message_id(self, post_id: int, review_message_id: int) -> None:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
await conn.execute(
|
|
"UPDATE posts SET review_message_id = $1 WHERE id = $2;",
|
|
review_message_id, post_id,
|
|
)
|
|
|
|
async def record_post_published_to_target(self, post_id: int, target_id: int, target_title: str) -> None:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
record_item = json.dumps({
|
|
"target_id": target_id,
|
|
"target_title": target_title,
|
|
"published_at": str(asyncpg.types.Type)
|
|
})
|
|
await conn.execute(
|
|
"""
|
|
UPDATE posts
|
|
SET published_to = published_to || $1::jsonb,
|
|
status = 'published',
|
|
published_at = CURRENT_TIMESTAMP
|
|
WHERE id = $2;
|
|
""",
|
|
f'[{{"target_id": {target_id}, "target_title": "{target_title}", "published_at": "{asyncpg.types.Type}"}}]',
|
|
post_id,
|
|
)
|
|
|
|
async def reject_post(self, post_id: int) -> None:
|
|
pool = await self._get_pool()
|
|
async with pool.acquire() as conn:
|
|
await conn.execute("UPDATE posts SET status = 'rejected' WHERE id = $1;", post_id)
|