rockybot / scripts /worker.py
youaremywind
Initial commit
1e7a182
Raw
History Blame Contribute Delete
7.64 kB
# ruff: noqa: E402
import asyncio
import logging
import re
import sys
import time
from pathlib import Path
from dotenv import load_dotenv
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
from datetime import datetime
from app.db.models import AIDetection, GroupConfig, MediaAsset, MessageEvent, StatsMessagesDaily, StatsWordcloudTerm
from app.db.session import SessionLocal
from app.services.ai_client import detect_image, detect_text
from app.services.queue import claim_job, complete_job, fail_job, get_pending_topics
from app.services.telegram_moderator import delete_message, punish_user
from app.services.telegram_sender import send_broadcast, send_post, send_scheduled_message
logger = logging.getLogger(__name__)
QUEUE_TOPICS = [
"ai_text_detect",
"ai_image_detect",
"moderation_action",
"broadcast_send",
"post_send",
"schedule_send",
"schedule_tick",
"stats_rollup",
]
async def _handle_ai_detection(message_id: str, kind: str) -> None:
db = SessionLocal()
try:
message = db.get(MessageEvent, message_id)
if not message:
return
config = db.query(GroupConfig).filter(GroupConfig.group_id == message.group_id).first()
ad_cfg = (config.config_json.get("ad_filter") if config else {}) or {}
if kind == "text" and not ad_cfg.get("text_ai_enabled", True):
return
if kind == "image" and not ad_cfg.get("image_ai_enabled", True):
return
threshold = float(ad_cfg.get("text_ai_threshold" if kind == "text" else "image_ai_threshold", 0.85))
score = 0.0
if kind == "text":
score = await detect_text(message.text or "")
else:
file_id = ""
if message.media_id:
asset = db.get(MediaAsset, message.media_id)
if asset:
file_id = asset.telegram_file_id
score = await detect_image(file_id)
result = "pass" if score < threshold else "reject"
db.add(
AIDetection(
group_id=message.group_id,
message_id=message.id,
type=kind,
score=score,
threshold=threshold,
model_id=None,
result=result,
)
)
db.commit()
if result == "reject" and message.chat_id:
action = ad_cfg.get("action", "delete")
duration = ad_cfg.get("action_duration")
if action == "delete":
await delete_message(
chat_id=message.chat_id,
message_id=message.telegram_message_id,
group_id=str(message.group_id),
user_id=message.telegram_user_id,
reason="AI广告过滤",
source="ai",
)
else:
await punish_user(
chat_id=message.chat_id,
user_id=message.telegram_user_id,
group_id=str(message.group_id),
action=action,
duration=duration,
reason="AI广告过滤",
source="ai",
)
finally:
db.close()
def _tokenize(text: str) -> list[str]:
tokens = re.findall(r"[A-Za-z0-9]+|[\u4e00-\u9fff]+", text)
return [token for token in tokens if len(token) >= 2]
def _handle_stats_rollup(message_id: str) -> None:
db = SessionLocal()
try:
message = db.get(MessageEvent, message_id)
if not message:
return
day = (message.created_at or datetime.utcnow()).date()
stats = (
db.query(StatsMessagesDaily)
.filter(StatsMessagesDaily.group_id == message.group_id, StatsMessagesDaily.date == day)
.first()
)
if not stats:
stats = StatsMessagesDaily(group_id=message.group_id, date=day, message_count=0, active_users=0)
db.add(stats)
stats.message_count += 1
active_users = (
db.query(MessageEvent.telegram_user_id)
.filter(MessageEvent.group_id == message.group_id)
.filter(MessageEvent.created_at >= datetime.combine(day, datetime.min.time()))
.filter(MessageEvent.created_at <= datetime.combine(day, datetime.max.time()))
.distinct()
.count()
)
stats.active_users = active_users
if message.text:
for token in _tokenize(message.text):
term = (
db.query(StatsWordcloudTerm)
.filter(
StatsWordcloudTerm.group_id == message.group_id,
StatsWordcloudTerm.date == day,
StatsWordcloudTerm.term == token,
)
.first()
)
if not term:
term = StatsWordcloudTerm(group_id=message.group_id, date=day, term=token, count=0)
db.add(term)
term.count += 1
db.commit()
finally:
db.close()
async def _dispatch(topic: str, payload: dict) -> None:
"""Dispatch a single queue message by topic."""
if topic == "post_send":
post_id = payload.get("post_id")
if post_id:
await send_post(post_id)
elif topic == "broadcast_send":
broadcast_id = payload.get("broadcast_id")
if broadcast_id:
await send_broadcast(broadcast_id)
elif topic == "schedule_send":
schedule_id = payload.get("schedule_id")
if schedule_id:
await send_scheduled_message(schedule_id)
elif topic == "ai_text_detect":
message_id = payload.get("message_id")
if message_id:
await _handle_ai_detection(message_id, "text")
elif topic == "ai_image_detect":
message_id = payload.get("message_id")
if message_id:
await _handle_ai_detection(message_id, "image")
elif topic == "stats_rollup":
message_id = payload.get("message_id")
if message_id:
# stats_rollup is sync (DB only), run in thread to avoid blocking
await asyncio.to_thread(_handle_stats_rollup, message_id)
else:
logger.info("[worker] %s %s", topic, payload)
async def worker_loop() -> None:
logger.info("worker started (PostgreSQL queue)")
while True:
try:
pending_topics = await asyncio.to_thread(get_pending_topics, QUEUE_TOPICS)
if not pending_topics:
await asyncio.sleep(0.5)
continue
for topic in pending_topics:
job = await asyncio.to_thread(claim_job, topic)
if not job:
continue
try:
await _dispatch(job.topic, job.payload)
await asyncio.to_thread(complete_job, job.id)
except Exception:
logger.exception("worker error processing %s", job.topic)
await asyncio.to_thread(fail_job, job.id)
except Exception:
logger.exception("worker loop error")
await asyncio.sleep(1)
def main():
"""Standalone entry point (backward compatible)."""
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
asyncio.run(worker_loop())
if __name__ == "__main__":
main()