Bot become a Community Guard & Post send manager

added: dictionary support for censore
message/user management with dict triggers
This commit is contained in:
2025-08-22 21:44:14 +09:00
parent efdafb0efa
commit c16ec54891
27 changed files with 1746 additions and 184 deletions

View File

@@ -1,14 +1,12 @@
from telegram import Update, InputMediaPhoto, InputMediaVideo, InputMediaAnimation
from telegram.constants import ParseMode, ChatType
from telegram.constants import ParseMode
from telegram.ext import ContextTypes
from app.db.session import get_session
from app.db.models import Draft, Chat, Delivery
from app.bot.keyboards.common import kb_choose_chat
from app.bot.messages import READY_SELECT_CHAT, SENT_OK, SEND_ERR, NEED_MEDIA_BEFORE_NEXT, CANCELLED
from .drafts import STATE_DRAFT, KEY_DRAFT_ID, STATE_AWAIT_TEXT, STATE_CONFIRM
from .add_group import STATE_KEY # чтобы не конфликтовать по user_data ключам
from app.db.models import Draft, Chat, Delivery, User
from app.bot.keyboards.common import kb_multiselect # ← только мультивыбор
from app.bot.messages import NEED_MEDIA_BEFORE_NEXT, NO_SELECTION, SENT_SUMMARY
from app.moderation.engine import check_message_allowed
async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
q = update.callback_query
@@ -26,11 +24,11 @@ async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
if len(d.media) == 0:
await q.edit_message_text(NEED_MEDIA_BEFORE_NEXT)
return
ctx.user_data[KEY_DRAFT_ID] = draft_id
ctx.user_data[STATE_DRAFT] = STATE_AWAIT_TEXT
ctx.user_data["draft_id"] = draft_id
ctx.user_data["draft_state"] = "await_text"
await q.edit_message_text("Шаг 2/3 — текст.\nОтправьте текст поста.")
# --- Подтверждение (Отправить) -> выбор чата ---
# --- Подтверждение -> мультивыбор чатов ---
elif data.startswith("draft_confirm_send:"):
draft_id = int(data.split(":")[1])
with get_session() as s:
@@ -40,32 +38,59 @@ async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
return
d.status = "ready"
s.commit()
# Разрешённые чаты владельца черновика
chats = s.query(Chat).filter_by(owner_user_id=d.user_id, can_post=True).all()
buttons = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
kb = kb_choose_chat(draft_id, buttons)
if not kb:
await q.edit_message_text("Нет чатов с правом публикации. Добавьте/обновите через /add_group.")
return
ctx.user_data[STATE_DRAFT] = STATE_CONFIRM
await q.edit_message_text(READY_SELECT_CHAT, reply_markup=kb)
chat_rows = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
sel_key = f"sel:{draft_id}"
ctx.user_data[sel_key] = set()
await q.edit_message_text("Выберите чаты:", reply_markup=kb_multiselect(draft_id, chat_rows, ctx.user_data[sel_key]))
# --- Тоггл чекбокса ---
elif data.startswith("tgl:"):
_, draft_id, chat_id = data.split(":")
draft_id = int(draft_id)
chat_id = int(chat_id)
sel_key = f"sel:{draft_id}"
# --- Отмена черновика ---
elif data.startswith("draft_cancel:"):
draft_id = int(data.split(":")[1])
with get_session() as s:
d = s.get(Draft, draft_id)
if d:
d.status = "cancelled"
s.commit()
ctx.user_data.pop(KEY_DRAFT_ID, None)
ctx.user_data.pop(STATE_DRAFT, None)
await q.edit_message_text(CANCELLED)
chats = s.query(Chat).filter_by(owner_user_id=d.user_id, can_post=True).all()
rows = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
# --- Отправка в выбранный чат ---
elif data.startswith("send:"):
_, draft_id, chat_id = data.split(":")
draft_id = int(draft_id); chat_id = int(chat_id)
sel: set[int] = set(ctx.user_data.get(sel_key, set()))
if chat_id in sel:
sel.remove(chat_id)
else:
sel.add(chat_id)
ctx.user_data[sel_key] = sel
await q.edit_message_reply_markup(reply_markup=kb_multiselect(draft_id, rows, sel))
# --- Выбрать все ---
elif data.startswith("selall:"):
draft_id = int(data.split(":")[1])
sel_key = f"sel:{draft_id}"
with get_session() as s:
d = s.get(Draft, draft_id)
chats = s.query(Chat).filter_by(owner_user_id=d.user_id, can_post=True).all()
rows = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
ctx.user_data[sel_key] = {cid for _, cid in rows}
await q.edit_message_reply_markup(reply_markup=kb_multiselect(draft_id, rows, ctx.user_data[sel_key]))
# --- Сброс ---
elif data.startswith("clear:"):
draft_id = int(data.split(":")[1])
sel_key = f"sel:{draft_id}"
with get_session() as s:
d = s.get(Draft, draft_id)
chats = s.query(Chat).filter_by(owner_user_id=d.user_id, can_post=True).all()
rows = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
ctx.user_data[sel_key] = set()
await q.edit_message_reply_markup(reply_markup=kb_multiselect(draft_id, rows, set()))
# --- Отправка выбранных ---
elif data.startswith("sendmulti:"):
draft_id = int(data.split(":")[1])
sel_key = f"sel:{draft_id}"
with get_session() as s:
d = s.get(Draft, draft_id)
@@ -73,46 +98,75 @@ async def on_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
await q.edit_message_text("Черновик не найден.")
return
chats = s.query(Chat).filter_by(owner_user_id=d.user_id, can_post=True).all()
rows = [(c.title or str(c.chat_id), c.chat_id) for c in chats]
selected: set[int] = ctx.user_data.get(sel_key, set())
if not selected:
await q.edit_message_text(NO_SELECTION, reply_markup=kb_multiselect(draft_id, rows, selected))
return
ok = 0
fail = 0
media = list(sorted(d.media, key=lambda m: m.order))
sent_ids = []
try:
if media:
if len(media) > 1:
im = []
for i, m in enumerate(media):
cap = d.text if i == 0 else None
if m.kind == "photo":
im.append(InputMediaPhoto(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
elif m.kind == "video":
im.append(InputMediaVideo(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
elif m.kind == "animation":
im.append(InputMediaAnimation(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
msgs = await ctx.bot.send_media_group(chat_id=chat_id, media=im)
sent_ids = [str(m.message_id) for m in msgs]
else:
m = media[0]
if m.kind == "photo":
msg = await ctx.bot.send_photo(chat_id=chat_id, photo=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
elif m.kind == "video":
msg = await ctx.bot.send_video(chat_id=chat_id, video=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
media_ids = [m.file_id for m in media]
text_val = d.text or ""
owner = s.get(User, d.user_id)
for cid in list(selected):
allowed, reasons, content_hash = check_message_allowed(
s, cid, owner_user_id=owner.id, text=text_val, media_ids=media_ids
)
if not allowed:
s.add(Delivery(draft_id=d.id, chat_id=cid, status="failed", error="; ".join(reasons), content_hash=content_hash))
s.commit()
fail += 1
continue
try:
if media:
if len(media) > 1:
im = []
for i, m in enumerate(media):
cap = text_val if i == 0 else None
if m.kind == "photo":
im.append(InputMediaPhoto(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
elif m.kind == "video":
im.append(InputMediaVideo(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
else:
im.append(InputMediaAnimation(media=m.file_id, caption=cap, parse_mode=ParseMode.HTML))
await ctx.bot.send_media_group(chat_id=cid, media=im)
else:
msg = await ctx.bot.send_animation(chat_id=chat_id, animation=m.file_id, caption=d.text, parse_mode=ParseMode.HTML)
sent_ids = [str(msg.message_id)]
else:
msg = await ctx.bot.send_message(chat_id=chat_id, text=d.text or "(пусто)", parse_mode=ParseMode.HTML)
sent_ids = [str(msg.message_id)]
m = media[0]
if m.kind == "photo":
await ctx.bot.send_photo(chat_id=cid, photo=m.file_id, caption=text_val, parse_mode=ParseMode.HTML)
elif m.kind == "video":
await ctx.bot.send_video(chat_id=cid, video=m.file_id, caption=text_val, parse_mode=ParseMode.HTML)
else:
await ctx.bot.send_animation(chat_id=cid, animation=m.file_id, caption=text_val, parse_mode=ParseMode.HTML)
else:
await ctx.bot.send_message(chat_id=cid, text=text_val or "(пусто)", parse_mode=ParseMode.HTML)
deliv = Delivery(draft_id=d.id, chat_id=chat_id, status="sent", message_ids=",".join(sent_ids))
s.add(deliv)
d.status = "sent"
s.add(Delivery(draft_id=d.id, chat_id=cid, status="sent", content_hash=content_hash))
s.commit()
ok += 1
except Exception as e:
s.add(Delivery(draft_id=d.id, chat_id=cid, status="failed", error=str(e), content_hash=content_hash))
s.commit()
fail += 1
ctx.user_data.pop(sel_key, None)
await q.edit_message_text(SENT_SUMMARY.format(ok=ok, fail=fail))
# --- Отмена ---
elif data.startswith("draft_cancel:"):
draft_id = int(data.split(":")[1])
with get_session() as s:
d = s.get(Draft, draft_id)
if d:
d.status = "cancelled"
s.commit()
# Сбрасываем пользовательское состояние редактора
ctx.user_data.pop(KEY_DRAFT_ID, None)
ctx.user_data.pop(STATE_DRAFT, None)
await q.edit_message_text(SENT_OK)
except Exception as e:
deliv = Delivery(draft_id=d.id, chat_id=chat_id, status="failed", error=str(e))
s.add(deliv); s.commit()
await q.edit_message_text(SEND_ERR.format(e=e))
ctx.user_data.pop(f"sel:{draft_id}", None)
ctx.user_data.pop("draft_id", None)
ctx.user_data.pop("draft_state", None)
await q.edit_message_text("Черновик отменён.")